"""Step definitions for plan lifecycle coverage scenarios.

Targets uncovered lines in:
- repositories.py: get_by_name, update (children), delete, list_plans filters,
  count, error paths, action update with inputs_schema/arguments/invariants
- plan_lifecycle_service.py: get_plan from persistence fallback,
  constrain_apply, auto_progress no-op, _persist_action_update (archive)
- unit_of_work.py: lifecycle_plans lazy property, add/flush/refresh
"""

from __future__ import annotations

import contextlib
from typing import Any

from behave import given, then, when
from behave.runner import Context
from sqlalchemy import create_engine, event
from sqlalchemy.orm import Session, sessionmaker

from cleveragents.application.services.plan_lifecycle_service import (
    PlanLifecycleService,
)
from cleveragents.config.settings import Settings
from cleveragents.domain.models.core.action import (
    ActionArgument,
    ArgumentRequirement,
    ArgumentType,
)
from cleveragents.domain.models.core.plan import (
    InvariantSource,
    PlanInvariant,
    PlanPhase,
    ProcessingState,
    ProjectLink,
)
from cleveragents.infrastructure.database.models import (
    Base,
    LifecyclePlanModel,
)
from cleveragents.infrastructure.database.repositories import (
    DuplicatePlanError,
    LifecyclePlanRepository,
    PlanNotFoundError,
)
from cleveragents.infrastructure.database.unit_of_work import UnitOfWork

# ── Helpers ──


def _build_cov_uow() -> tuple[UnitOfWork, sessionmaker[Session], Any]:
    """Build an in-memory UoW for coverage testing."""
    engine = create_engine(
        "sqlite:///:memory:",
        echo=False,
        future=True,
        connect_args={"check_same_thread": False},
    )

    @event.listens_for(engine, "connect")
    def _fk(dbapi_conn: Any, _rec: Any) -> None:
        cursor = dbapi_conn.cursor()
        cursor.execute("PRAGMA foreign_keys=ON")
        cursor.close()

    Base.metadata.create_all(engine)
    sf: sessionmaker[Session] = sessionmaker(
        bind=engine,
        expire_on_commit=False,
        autoflush=False,
        autocommit=False,
        class_=Session,
    )

    uow = UnitOfWork.__new__(UnitOfWork)
    uow.database_url = "sqlite:///:memory:"
    uow._engine = engine
    uow._session_factory = sf
    uow._database_initialized = True
    uow._prompt_for_migration = None

    return uow, sf, engine


# ── Background ──


@given("a coverage test service backed by SQLite")
def step_cov_bg(context: Context) -> None:
    uow, sf, engine = _build_cov_uow()
    settings = Settings()
    service = PlanLifecycleService(settings=settings, unit_of_work=uow)
    context.cov_service = service
    context.cov_uow = uow
    context.cov_sf = sf
    context.cov_engine = engine
    context.cov_plan = None
    context.cov_error = None
    context.cov_result = None
    context.cov_action_name = None


# ── Helpers for creating plans ──


def _create_cov_plan(context: Context) -> Any:
    """Create a plan via the service and store it on context."""
    svc: PlanLifecycleService = context.cov_service
    action_name = "local/cov-basic"
    with contextlib.suppress(Exception):
        svc.create_action(
            name=action_name,
            description="Coverage basic action",
            definition_of_done="Tests pass",
            strategy_actor="openai/gpt-4",
            execution_actor="openai/gpt-4",
            reusable=True,
        )
    plan = svc.use_action(action_name=action_name)
    context.cov_plan = plan
    context.cov_action_name = action_name
    return plan


@given("a coverage plan exists via the service")
def step_cov_plan_exists(context: Context) -> None:
    _create_cov_plan(context)


@given("a coverage plan with project links and invariants exists")
def step_cov_plan_with_children(context: Context) -> None:
    svc: PlanLifecycleService = context.cov_service
    action_name = "local/cov-children"
    with contextlib.suppress(Exception):
        svc.create_action(
            name=action_name,
            description="Action with children",
            definition_of_done="All green",
            strategy_actor="openai/gpt-4",
            execution_actor="openai/gpt-4",
            invariants=["Code must compile", "No regressions"],
            reusable=True,
        )

    links = [
        ProjectLink(project_name="local/api-service", alias="api", read_only=False),
        ProjectLink(project_name="local/frontend", alias="web", read_only=True),
    ]
    invariants = [
        PlanInvariant(text="Security audit required", source=InvariantSource.PLAN),
    ]
    plan = svc.use_action(
        action_name=action_name,
        project_links=links,
        invariants=invariants,
        arguments={},
    )
    context.cov_plan = plan
    context.cov_action_name = action_name


@given("a coverage plan in apply-queued state")
def step_cov_plan_apply_queued(context: Context) -> None:
    svc: PlanLifecycleService = context.cov_service
    action_name = "local/cov-apply-q"
    with contextlib.suppress(Exception):
        svc.create_action(
            name=action_name,
            description="Apply test action",
            definition_of_done="Done",
            strategy_actor="openai/gpt-4",
            execution_actor="openai/gpt-4",
            reusable=True,
        )
    plan = svc.use_action(action_name=action_name)
    pid = plan.identity.plan_id
    svc.start_strategize(pid)
    svc.complete_strategize(pid)
    svc.execute_plan(pid)
    svc.start_execute(pid)
    svc.complete_execute(pid)
    svc.apply_plan(pid)
    context.cov_plan = svc.get_plan(pid)


@given('a coverage action "{name}" exists')
def step_cov_action_exists(context: Context, name: str) -> None:
    svc: PlanLifecycleService = context.cov_service
    svc.create_action(
        name=name,
        description=f"Coverage action {name}",
        definition_of_done="Tests pass",
        strategy_actor="openai/gpt-4",
        execution_actor="openai/gpt-4",
        reusable=True,
    )


@given("a coverage action with inputs_schema and arguments exists")
def step_cov_action_with_inputs(context: Context) -> None:
    svc: PlanLifecycleService = context.cov_service
    args = [
        ActionArgument(
            name="target_dir",
            arg_type=ArgumentType.STRING,
            requirement=ArgumentRequirement.REQUIRED,
            description="Target directory",
            default_value="/tmp",
        ),
        ActionArgument(
            name="max_retries",
            arg_type=ArgumentType.INTEGER,
            requirement=ArgumentRequirement.OPTIONAL,
            description="Max retries",
            default_value=3,
        ),
    ]
    svc.create_action(
        name="local/cov-inputs-act",
        description="Action with inputs",
        definition_of_done="Done",
        strategy_actor="openai/gpt-4",
        execution_actor="openai/gpt-4",
        arguments=args,
        invariants=["Must not break API", "Must be backwards compatible"],
        reusable=True,
        tags=["test", "coverage"],
    )
    # Manually set inputs_schema on the action to cover line 924
    action = svc.get_action("local/cov-inputs-act")
    action.inputs_schema = {
        "type": "object",
        "properties": {"target_dir": {"type": "string"}},
    }
    context.cov_action_name = "local/cov-inputs-act"


# ── Get plan by name ──


@when("I retrieve the plan by its namespaced name from the repository")
def step_get_by_name(context: Context) -> None:
    plan = context.cov_plan
    name = str(plan.namespaced_name)
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    context.cov_result = repo.get_by_name(name)
    session.close()


@then("the plan retrieved by name should match the original")
def step_verify_get_by_name(context: Context) -> None:
    result = context.cov_result
    assert result is not None, "get_by_name returned None"
    assert result.identity.plan_id == context.cov_plan.identity.plan_id


@when('I retrieve a plan by name "{name}" from the repository')
def step_get_by_name_unknown(context: Context, name: str) -> None:
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    context.cov_result = repo.get_by_name(name)
    session.close()


@then("the plan-by-name result should be None")
def step_verify_get_by_name_none(context: Context) -> None:
    assert context.cov_result is None, f"Expected None, got {context.cov_result}"


# ── Update plan with children ──


@when("I update the plan description and phase through the repository")
def step_update_plan(context: Context) -> None:
    plan = context.cov_plan
    plan.description = "Updated description for coverage"
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    repo.update(plan)
    session.commit()
    session.close()


@then("the updated plan should have the new description in the DB")
def step_verify_updated_desc(context: Context) -> None:
    plan_id = context.cov_plan.identity.plan_id
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    loaded = repo.get(plan_id)
    session.close()
    assert loaded is not None
    assert loaded.description == "Updated description for coverage"


@then("the updated plan should retain project links")
def step_verify_project_links(context: Context) -> None:
    plan_id = context.cov_plan.identity.plan_id
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    loaded = repo.get(plan_id)
    session.close()
    assert loaded is not None
    links = getattr(loaded, "project_links", []) or []
    assert len(links) >= 2, f"Expected >= 2 project links, got {len(links)}"


@then("the updated plan should retain arguments")
def step_verify_arguments(context: Context) -> None:
    plan_id = context.cov_plan.identity.plan_id
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    loaded = repo.get(plan_id)
    session.close()
    assert loaded is not None
    # Arguments dict may be empty, but it should be present
    args = getattr(loaded, "arguments", None)
    assert args is not None, "arguments should not be None"


@then("the updated plan should retain invariants")
def step_verify_invariants(context: Context) -> None:
    plan_id = context.cov_plan.identity.plan_id
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    loaded = repo.get(plan_id)
    session.close()
    assert loaded is not None
    invs = getattr(loaded, "invariants", []) or []
    # Should have action invariants + plan invariants
    assert len(invs) >= 1, f"Expected >= 1 invariants, got {len(invs)}"


# ── Delete plan ──


@when("I delete the plan from the repository")
def step_delete_plan(context: Context) -> None:
    plan_id = context.cov_plan.identity.plan_id
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    context.cov_result = repo.delete(plan_id)
    session.commit()
    session.close()


@then("the plan should no longer be retrievable from the DB")
def step_verify_deleted(context: Context) -> None:
    assert context.cov_result is True, "delete should return True"
    plan_id = context.cov_plan.identity.plan_id
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    loaded = repo.get(plan_id)
    session.close()
    assert loaded is None, "Plan should be deleted"


@when('I delete a non-existent plan "{plan_id}" from the repository')
def step_delete_nonexistent(context: Context, plan_id: str) -> None:
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    context.cov_result = repo.delete(plan_id)
    session.close()


@then("the plan delete result should be false")
def step_verify_delete_false(context: Context) -> None:
    assert context.cov_result is False, f"Expected False, got {context.cov_result}"


# ── List plans with filters ──


@when('I list plans filtered by phase "{phase}"')
def step_list_by_phase(context: Context, phase: str) -> None:
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    context.cov_result = repo.list_plans(phase=phase)
    session.close()


@when('I list plans filtered by namespace "{namespace}"')
def step_list_by_namespace(context: Context, namespace: str) -> None:
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    context.cov_result = repo.list_plans(namespace=namespace)
    session.close()


@when("plcov I list plans filtered by action_name")
def step_list_by_action_name(context: Context) -> None:
    action_name = context.cov_action_name or str(context.cov_plan.action_name)
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    context.cov_result = repo.list_plans(action_name=action_name)
    session.close()


@then("the listed plans should include the coverage plan")
def step_verify_listed(context: Context) -> None:
    plans = context.cov_result
    assert len(plans) >= 1, f"Expected >= 1 plans, got {len(plans)}"
    plan_ids = [p.identity.plan_id for p in plans]
    assert context.cov_plan.identity.plan_id in plan_ids


# ── Count plans ──


@when("I count plans with no filters")
def step_count_no_filter(context: Context) -> None:
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    context.cov_result = repo.count()
    session.close()


@then("the count should be at least 1")
def step_verify_count(context: Context) -> None:
    assert context.cov_result >= 1, f"Expected >= 1, got {context.cov_result}"


@when('I count plans filtered by phase "{phase}"')
def step_count_by_phase(context: Context, phase: str) -> None:
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    context.cov_result = repo.count(phase=phase)
    session.close()


@when('I count plans filtered by processing_state "{state}"')
def step_count_by_state(context: Context, state: str) -> None:
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    context.cov_result = repo.count(processing_state=state)
    session.close()


@then("the filtered count should be at least 1")
def step_verify_filtered_count(context: Context) -> None:
    assert context.cov_result >= 1, f"Expected >= 1, got {context.cov_result}"


# ── Duplicate plan error ──


@when("I try to create a duplicate plan with the same ID")
def step_create_duplicate_plan(context: Context) -> None:
    plan = context.cov_plan
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    try:
        repo.create(plan)
        session.commit()
        context.cov_error = None
    except DuplicatePlanError as exc:
        context.cov_error = exc
    finally:
        session.close()


@then("a DuplicatePlanError should be raised")
def step_verify_duplicate_error(context: Context) -> None:
    assert context.cov_error is not None, "Expected DuplicatePlanError"
    assert isinstance(context.cov_error, DuplicatePlanError)


# ── Update non-existent plan error ──


@when("I try to update a plan that does not exist in the DB")
def step_update_nonexistent_plan(context: Context) -> None:
    from datetime import datetime

    from cleveragents.domain.models.core.plan import (
        NamespacedName,
        Plan,
        PlanIdentity,
        PlanTimestamps,
    )

    fake_plan = Plan(
        identity=PlanIdentity(plan_id="01ZZZZZZZZZZZZZZZZZZZZZZZZ"),
        namespaced_name=NamespacedName(
            server=None, namespace="local", name="fake-plan"
        ),
        action_name="local/fake-action",
        description="Fake plan",
        definition_of_done="Never",
        phase=PlanPhase.STRATEGIZE,
        processing_state=ProcessingState.QUEUED,
        strategy_actor="openai/gpt-4",
        execution_actor="openai/gpt-4",
        timestamps=PlanTimestamps(
            created_at=datetime.now(),
            updated_at=datetime.now(),
        ),
        reusable=True,
        read_only=False,
    )
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    try:
        repo.update(fake_plan)
        context.cov_error = None
    except PlanNotFoundError as exc:
        context.cov_error = exc
    finally:
        session.close()


@then("a PlanNotFoundError should be raised")
def step_verify_not_found_error(context: Context) -> None:
    assert context.cov_error is not None, "Expected PlanNotFoundError"
    assert isinstance(context.cov_error, PlanNotFoundError)


# ── Archive action (triggers ActionRepository.update) ──


@when('I archive the coverage action "{name}"')
def step_archive_action(context: Context, name: str) -> None:
    svc: PlanLifecycleService = context.cov_service
    context.cov_result = svc.archive_action(name)


@then('the action "{name}" should be archived in the DB')
def step_verify_archived(context: Context, name: str) -> None:
    from cleveragents.infrastructure.database.repositories import ActionRepository

    sf = context.cov_sf
    session = sf()
    repo = ActionRepository(session_factory=lambda: session)
    action = repo.get_by_name(name)
    session.close()
    assert action is not None, f"Action {name} not found"
    assert action.state.value == "archived" or str(action.state) == "archived"


# ── Action update with inputs_schema/arguments/invariants ──


@when("I update the coverage action with new description")
def step_update_action(context: Context) -> None:
    svc: PlanLifecycleService = context.cov_service
    action = svc.get_action("local/cov-inputs-act")
    action.description = "Updated description"
    # Persist via archive (which calls update in the repository)
    # Instead, let's call update directly via UoW to hit the exact lines
    with context.cov_uow.transaction() as ctx:
        ctx.actions.update(action)


@then("the action should retain its inputs_schema in the DB")
def step_verify_inputs_schema(context: Context) -> None:
    from cleveragents.infrastructure.database.repositories import ActionRepository

    sf = context.cov_sf
    session = sf()
    repo = ActionRepository(session_factory=lambda: session)
    action = repo.get_by_name("local/cov-inputs-act")
    session.close()
    assert action is not None
    assert action.inputs_schema is not None, "inputs_schema should be set"


@then("the action should retain its arguments with defaults in the DB")
def step_verify_action_args(context: Context) -> None:
    from cleveragents.infrastructure.database.repositories import ActionRepository

    sf = context.cov_sf
    session = sf()
    repo = ActionRepository(session_factory=lambda: session)
    action = repo.get_by_name("local/cov-inputs-act")
    session.close()
    assert action is not None
    assert len(action.arguments) >= 2, (
        f"Expected >= 2 args, got {len(action.arguments)}"
    )
    # Check default values were preserved
    defaults = [
        a.default_value for a in action.arguments if a.default_value is not None
    ]
    assert len(defaults) >= 1, "Should have at least 1 argument with default"


@then("the action should retain its invariants in the DB")
def step_verify_action_invariants(context: Context) -> None:
    from cleveragents.infrastructure.database.repositories import ActionRepository

    sf = context.cov_sf
    session = sf()
    repo = ActionRepository(session_factory=lambda: session)
    action = repo.get_by_name("local/cov-inputs-act")
    session.close()
    assert action is not None
    assert len(action.invariants) >= 2, (
        f"Expected >= 2 invariants, got {len(action.invariants)}"
    )


# ── Service get_plan from persistence fallback ──


@when("I clear the in-memory plan cache")
def step_clear_cache(context: Context) -> None:
    svc: PlanLifecycleService = context.cov_service
    # Clear in-memory plans dict to force fallback to persistence
    svc._plans.clear()


@when("I retrieve the plan by ID through the service")
def step_get_plan_by_id(context: Context) -> None:
    svc: PlanLifecycleService = context.cov_service
    plan_id = context.cov_plan.identity.plan_id
    context.cov_result = svc.get_plan(plan_id)


@then("the plan should be loaded from the persistence layer")
def step_verify_from_persistence(context: Context) -> None:
    result = context.cov_result
    assert result is not None, "Plan not loaded from persistence"
    assert result.identity.plan_id == context.cov_plan.identity.plan_id


# ── Constrain apply ──


@when('I constrain the apply with reason "{reason}"')
def step_constrain_apply(context: Context, reason: str) -> None:
    svc: PlanLifecycleService = context.cov_service
    plan_id = context.cov_plan.identity.plan_id
    # start the apply phase first
    svc.start_apply(plan_id)
    context.cov_plan = svc.constrain_apply(plan_id, reason)


@then('the coverage plan processing state should be "{state}"')
def step_cov_check_proc_state(context: Context, state: str) -> None:
    assert context.cov_plan.processing_state.value == state, (
        f"Expected {state}, got {context.cov_plan.processing_state.value}"
    )


@then('the coverage plan error message should be "{message}"')
def step_cov_check_error_msg(context: Context, message: str) -> None:
    assert context.cov_plan.error_message == message, (
        f"Expected '{message}', got '{context.cov_plan.error_message}'"
    )


# ── Auto progress no-op ──


@when("I call auto_progress on the plan")
def step_auto_progress(context: Context) -> None:
    svc: PlanLifecycleService = context.cov_service
    plan_id = context.cov_plan.identity.plan_id
    context.cov_result = svc.auto_progress(plan_id)


@then("the plan should be returned unchanged")
def step_verify_unchanged(context: Context) -> None:
    result = context.cov_result
    assert result is not None
    assert result.identity.plan_id == context.cov_plan.identity.plan_id
    # Phase should still be strategize/queued
    assert result.phase == PlanPhase.STRATEGIZE


# ── UoW lifecycle_plans property ──


@when("I open a UoW transaction and access lifecycle_plans")
def step_uow_lifecycle_plans(context: Context) -> None:
    uow = context.cov_uow
    with uow.transaction() as ctx:
        context.cov_result = ctx.lifecycle_plans


@then("the lifecycle_plans property should return a LifecyclePlanRepository")
def step_verify_lifecycle_plans_type(context: Context) -> None:
    assert isinstance(context.cov_result, LifecyclePlanRepository)


# ── UoW add/flush ──


@when("I use UoW context to add a raw model entity and flush")
def step_uow_add_flush(context: Context) -> None:
    from datetime import datetime

    # First, create an action (to satisfy FK constraint on v3_plans.action_name)
    svc: PlanLifecycleService = context.cov_service
    with contextlib.suppress(Exception):
        svc.create_action(
            name="local/uow-flush-test",
            description="UoW flush test action",
            definition_of_done="Done",
            strategy_actor="openai/gpt-4",
            execution_actor="openai/gpt-4",
            reusable=True,
        )

    uow = context.cov_uow
    with uow.transaction() as ctx:
        # Create a raw model and add it via the context
        model = LifecyclePlanModel()
        model.plan_id = "01TESTADD00000000000000000"  # type: ignore[assignment]
        model.root_plan_id = "01TESTADD00000000000000000"  # type: ignore[assignment]
        model.namespaced_name = "local/uow-add-test"  # type: ignore[assignment]
        model.namespace = "local"  # type: ignore[assignment]
        model.name = "uow-add-test"  # type: ignore[assignment]
        model.action_name = "local/uow-flush-test"  # type: ignore[assignment]
        model.description = "UoW add test"  # type: ignore[assignment]
        model.definition_of_done = "Pass"  # type: ignore[assignment]
        model.phase = "strategize"  # type: ignore[assignment]
        model.processing_state = "queued"  # type: ignore[assignment]
        model.attempt = 1  # type: ignore[assignment]
        model.strategy_actor = "openai/gpt-4"  # type: ignore[assignment]
        model.execution_actor = "openai/gpt-4"  # type: ignore[assignment]
        model.reusable = True  # type: ignore[assignment]
        model.read_only = False  # type: ignore[assignment]
        model.tags_json = "[]"  # type: ignore[assignment]
        model.sandbox_refs_json = "[]"  # type: ignore[assignment]
        model.effective_profile_snapshot = "{}"  # type: ignore[assignment]
        now = datetime.now().isoformat()
        model.created_at = now  # type: ignore[assignment]
        model.updated_at = now  # type: ignore[assignment]
        ctx.add(model)
        ctx.flush()
        context.cov_result = model.plan_id


@then("the entity should be queryable in the same session")
def step_verify_entity_added(context: Context) -> None:
    sf = context.cov_sf
    session = sf()
    repo = LifecyclePlanRepository(session_factory=lambda: session)
    plan = repo.get("01TESTADD00000000000000000")
    session.close()
    assert plan is not None, "Added entity should be queryable"
