"""Step definitions for changeset capture feature tests."""

import os
import tempfile
from datetime import UTC, datetime

from behave import given, then, when

from cleveragents.domain.models.core.change import (
    ChangeEntry,
    ChangeOperation,
    InMemoryChangeSetStore,
    SpecChangeSet,
)
from cleveragents.tool.builtins.changeset import (
    ChangeSetCapture,
    ChangeSetEntry,
)

# ---- ChangeOperation enum ----


@given("I import the ChangeOperation enum")
def step_import_change_operation(context):
    """Import ChangeOperation enum."""
    context.enum_cls = ChangeOperation


@then("it should have members CREATE, MODIFY, DELETE, RENAME")
def step_verify_enum_members(context):
    """Verify enum members."""
    assert hasattr(context.enum_cls, "CREATE")
    assert hasattr(context.enum_cls, "MODIFY")
    assert hasattr(context.enum_cls, "DELETE")
    assert hasattr(context.enum_cls, "RENAME")
    assert context.enum_cls.CREATE == "create"
    assert context.enum_cls.MODIFY == "modify"
    assert context.enum_cls.DELETE == "delete"
    assert context.enum_cls.RENAME == "rename"


# ---- ChangeEntry model ----


@given("I import the ChangeEntry model")
def step_import_change_entry(context):
    """Import ChangeEntry model."""
    context.entry_cls = ChangeEntry


@when("I create a ChangeEntry for a create operation")
def step_create_entry_create(context):
    """Create a ChangeEntry for create."""
    context.entry = ChangeEntry(
        plan_id="01HXYZ",
        resource_id="res-1",
        tool_name="builtin/file-write",
        operation=ChangeOperation.CREATE,
        path="src/new_file.py",
        after_hash="abc123",
    )


@then("the entry should have a ULID entry_id")
def step_entry_has_ulid(context):
    """Verify ULID entry_id."""
    assert context.entry.entry_id
    assert len(context.entry.entry_id) > 0


@then('the entry should have operation "{op}"')
def step_entry_operation(context, op):
    """Verify operation value."""
    assert context.entry.operation == op


@then("the entry should have a UTC timestamp")
def step_entry_utc_timestamp(context):
    """Verify UTC timestamp."""
    ts = context.entry.timestamp
    assert isinstance(ts, datetime)
    assert ts.tzinfo is not None
    assert ts.tzinfo in (UTC, UTC)


@then("the before_hash should be None")
def step_before_hash_none(context):
    """Verify before_hash is None."""
    assert context.entry.before_hash is None


@when("I create a ChangeEntry for a modify operation with hashes")
def step_create_entry_modify(context):
    """Create a ChangeEntry for modify with hashes."""
    context.entry = ChangeEntry(
        plan_id="01HXYZ",
        resource_id="res-1",
        tool_name="builtin/file-edit",
        operation=ChangeOperation.MODIFY,
        path="src/existing.py",
        before_hash="hash-before",
        after_hash="hash-after",
    )


@then("the before_hash should not be None")
def step_before_hash_not_none(context):
    """Verify before_hash is set."""
    assert context.entry.before_hash is not None


@then("the after_hash should not be None")
def step_after_hash_not_none(context):
    """Verify after_hash is set."""
    assert context.entry.after_hash is not None


@when("I create a ChangeEntry for a delete operation")
def step_create_entry_delete(context):
    """Create a ChangeEntry for delete."""
    context.entry = ChangeEntry(
        plan_id="01HXYZ",
        resource_id="res-1",
        tool_name="builtin/file-delete",
        operation=ChangeOperation.DELETE,
        path="src/old_file.py",
        before_hash="hash-of-deleted",
    )


@then("the after_hash should be None")
def step_after_hash_none(context):
    """Verify after_hash is None."""
    assert context.entry.after_hash is None


@when("I create a ChangeEntry for a rename operation")
def step_create_entry_rename(context):
    """Create a ChangeEntry for rename."""
    context.entry = ChangeEntry(
        plan_id="01HXYZ",
        resource_id="res-1",
        tool_name="builtin/file-move",
        operation=ChangeOperation.RENAME,
        path="src/old_name.py",
        before_hash="hash-x",
        after_hash="hash-x",
    )


# ---- SpecChangeSet model ----


@given("I import the SpecChangeSet model")
def step_import_spec_changeset(context):
    """Import SpecChangeSet."""
    context.cs_cls = SpecChangeSet


def _make_mixed_entries():
    """Build a list of entries with mixed operations."""
    base = {
        "plan_id": "plan-1",
        "resource_id": "res-A",
        "tool_name": "builtin/file-write",
    }
    return [
        ChangeEntry(
            **base,
            operation=ChangeOperation.CREATE,
            path="a.py",
        ),
        ChangeEntry(
            **base,
            operation=ChangeOperation.MODIFY,
            path="b.py",
        ),
        ChangeEntry(
            plan_id="plan-1",
            resource_id="res-B",
            tool_name="builtin/file-edit",
            operation=ChangeOperation.MODIFY,
            path="c.py",
        ),
        ChangeEntry(
            **base,
            operation=ChangeOperation.DELETE,
            path="d.py",
        ),
        ChangeEntry(
            **base,
            operation=ChangeOperation.RENAME,
            path="e.py",
        ),
    ]


@when("I create a SpecChangeSet with mixed operations")
def step_create_mixed_changeset(context):
    """Create a SpecChangeSet with mixed operations."""
    context.spec_cs = SpecChangeSet(
        plan_id="plan-1",
        entries=_make_mixed_entries(),
    )


@then("the creates count should be {n:d}")
def step_creates_count(context, n):
    """Verify creates count."""
    assert context.spec_cs.creates == n


@then("the modifies count should be {n:d}")
def step_modifies_count(context, n):
    """Verify modifies count."""
    assert context.spec_cs.modifies == n


@then("the deletes count should be {n:d}")
def step_deletes_count(context, n):
    """Verify deletes count."""
    assert context.spec_cs.deletes == n


@then("the renames count should be {n:d}")
def step_renames_count(context, n):
    """Verify renames count."""
    assert context.spec_cs.renames == n


@then("paths_changed should have {n:d} entries")
def step_paths_changed(context, n):
    """Verify paths_changed count."""
    assert len(context.spec_cs.paths_changed) == n


@then("resources_involved should have {n:d} entries")
def step_resources_involved(context, n):
    """Verify resources_involved count."""
    assert len(context.spec_cs.resources_involved) == n


@then("the summary dict should have correct totals")
def step_summary_dict(context):
    """Verify summary dict."""
    s = context.spec_cs.summary()
    assert s["total"] == 5
    assert s["creates"] == 1
    assert s["modifies"] == 2
    assert s["deletes"] == 1
    assert s["renames"] == 1
    assert s["paths_changed"] == 5
    assert s["resources_involved"] == 2


@when("I create an empty SpecChangeSet")
def step_create_empty_changeset(context):
    """Create an empty SpecChangeSet."""
    context.spec_cs = SpecChangeSet(plan_id="plan-empty")


# ---- InMemoryChangeSetStore ----


@given("I have an InMemoryChangeSetStore")
def step_create_store(context):
    """Create InMemoryChangeSetStore."""
    context.store = InMemoryChangeSetStore()
    context.changeset_ids = []


@when('I start a changeset for plan "{plan_id}"')
def step_start_changeset(context, plan_id):
    """Start a changeset."""
    cs_id = context.store.start(plan_id)
    context.changeset_ids.append(cs_id)
    context.last_cs_id = cs_id


@when("I record a create entry in the changeset")
def step_record_create_entry(context):
    """Record a create entry."""
    # Use the plan_id from the changeset so the entry matches
    # (InMemoryChangeSetStore.record now validates via add_change).
    cs = context.store.get(context.last_cs_id)
    plan_id = cs.plan_id if cs is not None else "plan-abc"
    entry = ChangeEntry(
        plan_id=plan_id,
        resource_id="res-1",
        tool_name="builtin/file-write",
        operation=ChangeOperation.CREATE,
        path="new.py",
    )
    context.store.record(context.last_cs_id, entry)


@then("I can get the changeset by ID")
def step_get_changeset(context):
    """Verify get returns the changeset."""
    cs = context.store.get(context.last_cs_id)
    assert cs is not None
    context.fetched_cs = cs


@then("the store changeset should have {n:d} entry")
def step_store_changeset_entry_count(context, n):
    """Verify entry count in store."""
    cs = context.store.get(context.last_cs_id)
    assert cs is not None
    assert len(cs.entries) == n


@then('get_for_plan "{plan_id}" should return {n:d} changesets')
def step_get_for_plan_count(context, plan_id, n):
    """Verify get_for_plan returns expected count."""
    result = context.store.get_for_plan(plan_id)
    assert len(result) == n


@then('get_for_plan "{plan_id}" should return {n:d} changeset')
def step_get_for_plan_singular(context, plan_id, n):
    """Verify get_for_plan returns expected count (singular)."""
    result = context.store.get_for_plan(plan_id)
    assert len(result) == n


@when("I record a modify entry in the changeset")
def step_record_modify_entry(context):
    """Record a modify entry."""
    entry = ChangeEntry(
        plan_id="plan-s",
        resource_id="res-1",
        tool_name="builtin/file-edit",
        operation=ChangeOperation.MODIFY,
        path="existing.py",
        before_hash="aaa",
        after_hash="bbb",
    )
    context.store.record(context.last_cs_id, entry)


@then("the summarized changeset should show {n:d} total")
def step_summarize_total(context, n):
    """Verify summarize total."""
    s = context.store.summarize(context.last_cs_id)
    assert s["total"] == n


@then("getting a non-existent changeset returns None")
def step_get_nonexistent(context):
    """Verify get returns None for unknown ID."""
    assert context.store.get("nonexistent-id") is None


@then("recording to a non-existent changeset raises KeyError")
def step_record_nonexistent(context):
    """Verify recording to unknown ID raises KeyError."""
    entry = ChangeEntry(
        plan_id="plan-x",
        resource_id="res-1",
        tool_name="t",
        operation=ChangeOperation.CREATE,
        path="x.py",
    )
    try:
        context.store.record("nonexistent-id", entry)
        raise AssertionError("Expected KeyError")
    except KeyError:
        pass


@then("summarizing a non-existent changeset returns empty dict")
def step_summarize_nonexistent(context):
    """Verify summarize returns {} for unknown ID."""
    assert context.store.summarize("nonexistent-id") == {}


# ---- ChangeSetCapture integration ----


@given('I have a ChangeSetCapture with resource_id "{rid}"')
def step_capture_with_resource(context, rid):
    """Create ChangeSetCapture with resource_id."""
    context.capture = ChangeSetCapture(
        plan_id="plan-cap",
        resource_id=rid,
    )


@when("I create a ChangeSetEntry via capture")
def step_create_via_capture(context):
    """Add an entry via the capture object."""
    entry = ChangeSetEntry(
        operation="create",
        path="test.py",
        resource_id=None,
        tool_name="builtin/file-write",
    )
    context.capture._entries.append(entry)
    # Resource ID comes from capture default
    context.last_entry = context.capture.get_changeset().entries[-1]


@then('the entry resource_id should be "{rid}"')
def step_entry_resource_id(context, rid):
    """Verify resource_id is empty — set on domain convert."""
    # The lightweight entry may have None; the capture stores
    # the resource_id on the capture object itself.
    assert context.capture._resource_id == rid


@then("the entry tool_name should be set")
def step_entry_tool_name_set(context):
    """Verify tool_name is set."""
    assert context.last_entry.tool_name is not None


@given("I have a ChangeSetCapture with sandbox_root")
def step_capture_with_sandbox(context):
    """Create capture with a sandbox root."""
    context.tmpdir = tempfile.mkdtemp()
    context.capture = ChangeSetCapture(
        plan_id="plan-sb",
        resource_id="res-sb",
        sandbox_root=context.tmpdir,
    )


@when("I capture a write to a nested path")
def step_capture_nested_write(context):
    """Simulate capturing a nested path write."""
    from cleveragents.tool.builtins.changeset import (
        _normalize_path,
    )

    context.normalized = _normalize_path(
        "sub/dir/file.py", context.capture._sandbox_root
    )


@then("the captured path should be repo-relative")
def step_path_repo_relative(context):
    """Verify the path is relative."""
    assert not os.path.isabs(context.normalized)
    assert context.normalized == os.path.join("sub", "dir", "file.py")


@when("I add several entries via capture")
def step_add_entries_via_capture(context):
    """Add several entries directly."""
    for op in ("create", "modify", "delete"):
        entry = ChangeSetEntry(
            operation=op,
            path=f"{op}_file.py",
            resource_id="res-2",
            tool_name="builtin/file-write",
        )
        context.capture._entries.append(entry)


@then("to_spec_changeset should return a SpecChangeSet")
def step_spec_changeset_conversion(context):
    """Verify spec changeset conversion."""
    spec = context.capture.to_spec_changeset()
    assert isinstance(spec, SpecChangeSet)
    assert len(spec.entries) == 3
    assert spec.plan_id == "plan-cap"


# ---- Multi-resource plan ----


@given("I have two ChangeSetCapture instances for different resources")
def step_two_captures(context):
    """Create two captures for different resources."""
    context.capture_a = ChangeSetCapture(
        plan_id="plan-multi",
        resource_id="res-A",
    )
    context.capture_b = ChangeSetCapture(
        plan_id="plan-multi",
        resource_id="res-B",
    )


@when("each capture records changes for its resource")
def step_record_per_resource(context):
    """Record one entry in each capture."""
    entry_a = ChangeSetEntry(
        operation="create",
        path="a.py",
        resource_id="res-A",
        tool_name="builtin/file-write",
    )
    entry_b = ChangeSetEntry(
        operation="modify",
        path="b.py",
        resource_id="res-B",
        tool_name="builtin/file-edit",
    )
    context.capture_a._entries.append(entry_a)
    context.capture_b._entries.append(entry_b)


@then("each changeset should only have its resource entries")
def step_verify_per_resource(context):
    """Verify each changeset has only its resource."""
    cs_a = context.capture_a.get_changeset()
    cs_b = context.capture_b.get_changeset()
    assert len(cs_a.entries) == 1
    assert cs_a.entries[0].resource_id == "res-A"
    assert len(cs_b.entries) == 1
    assert cs_b.entries[0].resource_id == "res-B"
