"""Step definitions for lifecycle data persistence and retrieval tests."""

import json
from datetime import datetime

from behave import given, then, when
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from cleveragents.infrastructure.database.models import (
    ActionArgumentModel,
    Base,
    LifecycleActionModel,
    LifecyclePlanModel,
    PlanProjectModel,
)

# ---------------------------------------------------------------------------
# Background steps
# ---------------------------------------------------------------------------


@given("the lifecycle database is ready")
def step_import_lifecycle_modules(context):
    """Set up an in-memory database with lifecycle tables."""
    context.database_url = "sqlite:///:memory:"
    context.engine = create_engine(context.database_url)
    context.SessionLocal = sessionmaker(
        bind=context.engine, autoflush=False, autocommit=False
    )
    Base.metadata.create_all(context.engine)


@given("a lifecycle database session is open")
def step_create_lifecycle_test_session(context):
    """Open a database session for the lifecycle tests."""
    if not hasattr(context, "SessionLocal"):
        step_import_lifecycle_modules(context)
    context.db_session = context.SessionLocal()


# ---------------------------------------------------------------------------
# Helper: create a fully populated LifecycleActionModel
# ---------------------------------------------------------------------------


VALID_ULID_1 = "01HGZ6FE0AQDYTR4BXVQZ6E001"
VALID_ULID_2 = "01HGZ6FE0AQDYTR4BXVQZ6E002"
VALID_ULID_3 = "01HGZ6FE0AQDYTR4BXVQZ6E003"
VALID_ULID_4 = "01HGZ6FE0AQDYTR4BXVQZ6E004"
VALID_ULID_5 = "01HGZ6FE0AQDYTR4BXVQZ6E005"


def _make_action_model(
    name="local/test-action",
    namespace="local",
    short_name="test-action",
    description="A short description",
    long_description="A long description of the action",
    definition_of_done="All tests pass and coverage > 80%",
    strategy_actor="local/strategy-actor",
    execution_actor="local/execution-actor",
    estimation_actor="local/estimation-actor",
    review_actor="local/review-actor",
    inputs_schema="[]",
    state="available",
    reusable=True,
    read_only=False,
    created_at="2025-06-01T12:00:00",
    updated_at="2025-06-01T13:00:00",
    created_by="test-user",
    tags="[]",
):
    """Create a LifecycleActionModel with sensible defaults.

    Maps the legacy helper parameters to the new spec-aligned column names:
    - ``name`` -> ``namespaced_name`` (PK)
    - ``short_name`` -> ``name``
    - ``description`` -> ``description``
    - ``inputs_schema`` -> ``inputs_schema_json``
    - ``tags`` -> ``tags_json``
    """
    return LifecycleActionModel(
        namespaced_name=name,
        namespace=namespace,
        name=short_name,
        description=description,
        long_description=long_description,
        definition_of_done=definition_of_done,
        strategy_actor=strategy_actor,
        execution_actor=execution_actor,
        estimation_actor=estimation_actor,
        review_actor=review_actor,
        inputs_schema_json=inputs_schema if inputs_schema != "[]" else None,
        state=state,
        reusable=reusable,
        read_only=read_only,
        created_at=created_at,
        updated_at=updated_at,
        created_by=created_by,
        tags_json=tags,
    )


def _make_plan_model(
    plan_id=VALID_ULID_1,
    parent_plan_id=None,
    root_plan_id=None,
    action_name="local/test-action",
    phase="strategize",
    state="queued",
    attempt=1,
    namespaced_name="local/test-plan",
    namespace="local",
    description="A test plan description",
    definition_of_done="Tests pass",
    project_names=None,
    strategy_actor="local/strategy-actor",
    execution_actor="local/execution-actor",
    error_message=None,
    created_at="2025-06-01T12:00:00",
    updated_at="2025-06-01T13:00:00",
    completed_at=None,
    strategize_started_at=None,
    strategize_completed_at=None,
    execute_started_at=None,
    execute_completed_at=None,
    apply_started_at=None,
    applied_at=None,
    created_by="test-user",
    tags="[]",
    reusable=True,
    read_only=False,
):
    """Create a LifecyclePlanModel with sensible defaults.

    Maps legacy helper parameters to the new spec-aligned column names:
    - ``action_id`` -> ``action_name`` (FK to actions.namespaced_name)
    - ``state`` -> ``processing_state``
    - ``project_ids`` removed (now uses ``plan_projects`` child table)
    - ``tags`` -> ``tags_json``
    - Added ``namespace`` column
    """
    # Root plans self-reference; default when not explicitly provided.
    resolved_root = root_plan_id if root_plan_id is not None else plan_id
    model = LifecyclePlanModel(
        plan_id=plan_id,
        parent_plan_id=parent_plan_id,
        root_plan_id=resolved_root,
        action_name=action_name,
        phase=phase,
        processing_state=state,
        attempt=attempt,
        namespaced_name=namespaced_name,
        namespace=namespace,
        description=description,
        definition_of_done=definition_of_done,
        strategy_actor=strategy_actor,
        execution_actor=execution_actor,
        error_message=error_message,
        created_at=created_at,
        updated_at=updated_at,
        completed_at=completed_at,
        strategize_started_at=strategize_started_at,
        strategize_completed_at=strategize_completed_at,
        execute_started_at=execute_started_at,
        execute_completed_at=execute_completed_at,
        apply_started_at=apply_started_at,
        applied_at=applied_at,
        created_by=created_by,
        tags_json=tags,
        reusable=reusable,
        read_only=read_only,
        effective_profile_snapshot="{}",
    )
    # Populate project links via child table
    if project_names:
        for pname in json.loads(project_names):
            model.project_links_rel.append(
                PlanProjectModel(
                    project_name=pname,
                    created_at=created_at,
                )
            )
    return model


# ---------------------------------------------------------------------------
# Action: loading a stored record as a domain object
# ---------------------------------------------------------------------------


@given("an action record exists with valid attributes and tags")
def step_create_action_model_valid(context):
    """Persist a valid action record with tags."""
    context.action_model = _make_action_model(
        tags='["tag1", "tag2"]',
    )
    context.db_session.add(context.action_model)
    context.db_session.commit()


@when("the action record is loaded as a domain object")
def step_call_to_domain_on_action(context):
    """Load the persisted action record as a domain object."""
    context.action_domain = context.action_model.to_domain()


@then("the loaded action should preserve its original identifier")
def step_verify_action_domain_id(context):
    """Verify the loaded action retains the original identifier (namespaced_name)."""
    assert str(context.action_domain.namespaced_name) == "local/test-action"


@then("the loaded action should preserve its namespace and short name")
def step_verify_action_domain_namespaced_name(context):
    """Verify the loaded action retains its namespace and short name."""
    assert context.action_domain.namespaced_name.namespace == "local"
    assert context.action_domain.namespaced_name.name == "test-action"


@then("the loaded action should preserve its description fields")
def step_verify_action_domain_descriptions(context):
    """Verify the loaded action retains all description fields."""
    assert context.action_domain.description == "A short description"
    assert context.action_domain.long_description == "A long description of the action"
    assert (
        context.action_domain.definition_of_done == "All tests pass and coverage > 80%"
    )


@then("the loaded action should preserve its actor assignments")
def step_verify_action_domain_actors(context):
    """Verify the loaded action retains all actor assignments."""
    assert context.action_domain.strategy_actor == "local/strategy-actor"
    assert context.action_domain.execution_actor == "local/execution-actor"
    assert context.action_domain.estimation_actor == "local/estimation-actor"
    assert context.action_domain.review_actor == "local/review-actor"


@then("the loaded action should preserve its state and flags")
def step_verify_action_domain_state_flags(context):
    """Verify the loaded action retains its state and boolean flags."""
    from cleveragents.domain.models.core.action import ActionState

    assert context.action_domain.state == ActionState.AVAILABLE
    assert context.action_domain.reusable is True
    assert context.action_domain.read_only is False


@then("the loaded action should preserve its timestamps")
def step_verify_action_domain_timestamps(context):
    """Verify the loaded action retains its timestamps."""
    assert context.action_domain.created_at == datetime.fromisoformat(
        "2025-06-01T12:00:00"
    )
    assert context.action_domain.updated_at == datetime.fromisoformat(
        "2025-06-01T13:00:00"
    )


@then("the loaded action should preserve its tags")
def step_verify_action_domain_tags(context):
    """Verify the loaded action retains its tags."""
    assert context.action_domain.tags == ["tag1", "tag2"]


# ---------------------------------------------------------------------------
# Action: loading a stored record with arguments
# ---------------------------------------------------------------------------


@given("an action record exists with a structured arguments schema")
def step_create_action_model_with_args(context):
    """Persist an action record that has structured input arguments via child table."""
    context.action_model = _make_action_model()
    # Add arguments via the child table
    context.action_model.arguments_rel.append(
        ActionArgumentModel(
            name="target_coverage",
            arg_type="integer",
            requirement="required",
            description="Target coverage percentage",
            position=0,
        )
    )
    context.action_model.arguments_rel.append(
        ActionArgumentModel(
            name="framework",
            arg_type="string",
            requirement="optional",
            description="Test framework to use",
            position=1,
        )
    )
    context.db_session.add(context.action_model)
    context.db_session.commit()


@then("the loaded action should contain the expected argument entries")
def step_verify_action_arguments_parsed(context):
    """Verify the loaded action contains the expected number of arguments."""
    assert len(context.action_domain.arguments) == 2


@then("each argument entry should have the correct name and type")
def step_verify_action_argument_details(context):
    """Verify each argument entry has the correct name."""
    args = context.action_domain.arguments
    assert args[0].name == "target_coverage"
    assert args[1].name == "framework"


# ---------------------------------------------------------------------------
# Action: loading a record with no inputs or tags
# ---------------------------------------------------------------------------


@given("an action record exists with no inputs and no tags")
def step_create_action_model_empty_inputs_tags(context):
    """Persist an action record with empty inputs and tags."""
    context.action_model = _make_action_model(inputs_schema="[]", tags="[]")
    context.db_session.add(context.action_model)
    context.db_session.commit()


@then("the loaded action should have an empty arguments collection")
def step_verify_empty_arguments(context):
    """Verify the loaded action has no arguments."""
    assert context.action_domain.arguments == []


@then("the loaded action should have an empty tags collection")
def step_verify_empty_tags(context):
    """Verify the loaded action has no tags."""
    assert context.action_domain.tags == []


# ---------------------------------------------------------------------------
# Action: storing a domain object as a database record
# ---------------------------------------------------------------------------


@given("a complete action domain object is prepared")
def step_create_action_domain_object(context):
    """Prepare a fully populated action domain object."""
    from cleveragents.domain.models.core.action import (
        Action,
        ActionArgument,
        ActionState,
    )
    from cleveragents.domain.models.core.plan import NamespacedName

    context.action_domain_input = Action(
        namespaced_name=NamespacedName(namespace="local", name="my-action"),
        description="Short desc",
        long_description="Long desc",
        definition_of_done="All tests pass",
        strategy_actor="local/strategy",
        execution_actor="local/executor",
        estimation_actor="local/estimator",
        review_actor="local/reviewer",
        arguments=[
            ActionArgument(
                name="coverage",
                arg_type="int",
                requirement="required",
                description="Coverage target",
            ),
        ],
        reusable=True,
        read_only=False,
        state=ActionState.AVAILABLE,
        created_at=datetime(2025, 6, 1, 12, 0, 0),
        updated_at=datetime(2025, 6, 1, 13, 0, 0),
        created_by="test-user",
        tags=["ci", "testing"],
    )


@when("the action domain object is stored as a database record")
def step_call_from_domain_on_action(context):
    """Store the action domain object as a database record."""
    context.action_model_result = LifecycleActionModel.from_domain(
        context.action_domain_input
    )


@then("the stored record should preserve the action identifier")
def step_verify_from_domain_action_id(context):
    """Verify the stored record retains the action identifier (namespaced_name)."""
    assert context.action_model_result.namespaced_name == "local/my-action"


@then("the stored record should preserve the name components")
def step_verify_from_domain_name_fields(context):
    """Verify the stored record retains name, namespace, and short name."""
    assert context.action_model_result.namespaced_name == "local/my-action"
    assert context.action_model_result.namespace == "local"
    assert context.action_model_result.name == "my-action"


@then("the stored record should preserve the description fields")
def step_verify_from_domain_descriptions(context):
    """Verify the stored record retains all description fields."""
    assert context.action_model_result.description == "Short desc"
    assert context.action_model_result.long_description == "Long desc"
    assert context.action_model_result.definition_of_done == "All tests pass"


@then("the stored record should preserve the actor assignments")
def step_verify_from_domain_actors(context):
    """Verify the stored record retains all actor assignments."""
    assert context.action_model_result.strategy_actor == "local/strategy"
    assert context.action_model_result.execution_actor == "local/executor"
    assert context.action_model_result.estimation_actor == "local/estimator"
    assert context.action_model_result.review_actor == "local/reviewer"


@then("the stored record should serialize the arguments as JSON")
def step_verify_from_domain_inputs_schema(context):
    """Verify the stored record has arguments stored via child table."""
    args = context.action_model_result.arguments_rel
    assert len(args) == 1
    assert args[0].name == "coverage"


@then("the stored record should serialize the tags as JSON")
def step_verify_from_domain_tags_json(context):
    """Verify the stored record has tags serialized as JSON."""
    parsed = json.loads(context.action_model_result.tags_json)
    assert parsed == ["ci", "testing"]


@then("the stored record should preserve the state value")
def step_verify_from_domain_state(context):
    """Verify the stored record retains the state value."""
    assert context.action_model_result.state == "available"


@then("the stored record should format timestamps as ISO strings")
def step_verify_from_domain_timestamps(context):
    """Verify the stored record formats timestamps as ISO strings."""
    assert context.action_model_result.created_at == "2025-06-01T12:00:00"
    assert context.action_model_result.updated_at == "2025-06-01T13:00:00"


# ---------------------------------------------------------------------------
# Action: storing with an enumerated state
# ---------------------------------------------------------------------------


@given("an action domain object is prepared with an enumerated state")
def step_create_action_with_enum_state(context):
    """Prepare an action domain object that uses an ActionState enum value."""
    from cleveragents.domain.models.core.action import Action, ActionState
    from cleveragents.domain.models.core.plan import NamespacedName

    context.action_domain_input = Action(
        namespaced_name=NamespacedName(namespace="local", name="enum-action"),
        description="Enum state action",
        definition_of_done="Tests pass",
        strategy_actor="local/strategy",
        execution_actor="local/executor",
        state=ActionState.AVAILABLE,
        created_at=datetime(2025, 6, 1, 12, 0, 0),
        updated_at=datetime(2025, 6, 1, 13, 0, 0),
    )


@then("the stored record state should equal the enum value")
def step_verify_enum_state_value(context):
    """Verify the stored record state is the string value of the enum."""
    assert context.action_model_result.state == "available"


# ---------------------------------------------------------------------------
# Action: storing with a custom string state
# ---------------------------------------------------------------------------


@given("an action-like object is prepared with a custom string state")
def step_create_action_with_string_state(context):
    """Prepare an action-like object with a plain string state."""
    from types import SimpleNamespace

    from cleveragents.domain.models.core.plan import NamespacedName

    context.action_domain_string_state = SimpleNamespace(
        namespaced_name=NamespacedName(namespace="local", name="str-action"),
        description="Desc",
        long_description="Long",
        definition_of_done="Done",
        strategy_actor="local/strategy",
        execution_actor="local/executor",
        estimation_actor=None,
        review_actor=None,
        apply_actor=None,
        invariant_actor=None,
        arguments=[],
        inputs_schema=None,
        invariants=[],
        automation_profile=None,
        reusable=True,
        read_only=False,
        state="custom-state",
        created_at=datetime(2025, 6, 1, 12, 0, 0),
        updated_at=datetime(2025, 6, 1, 13, 0, 0),
        created_by=None,
        tags=[],
    )


@when("the custom-state action is stored as a database record")
def step_call_from_domain_string_state(context):
    """Store the custom-state action as a database record."""
    context.action_model_result = LifecycleActionModel.from_domain(
        context.action_domain_string_state
    )


@then("the stored record state should equal the custom string")
def step_verify_plain_string_state(context):
    """Verify the stored record state is the plain custom string."""
    assert context.action_model_result.state == "custom-state"


# ---------------------------------------------------------------------------
# Plan: parsing ISO timestamps
# ---------------------------------------------------------------------------


@when("a valid ISO-8601 string is parsed as a timestamp")
def step_call_parse_iso_valid(context):
    """Parse a valid ISO-8601 string as a timestamp."""
    context.parse_iso_result = LifecyclePlanModel._parse_iso("2025-06-01T12:00:00")


@then("the parsed timestamp should be the expected datetime value")
def step_verify_parse_iso_datetime(context):
    """Verify the parsed timestamp matches the expected datetime."""
    assert isinstance(context.parse_iso_result, datetime)
    assert context.parse_iso_result == datetime(2025, 6, 1, 12, 0, 0)


@when("an absent value is parsed as a timestamp")
def step_call_parse_iso_none(context):
    """Parse an absent (None) value as a timestamp."""
    context.parse_iso_result = LifecyclePlanModel._parse_iso(None)


@then("the parsed timestamp should be absent")
def step_verify_parse_iso_none(context):
    """Verify parsing an absent value returns nothing."""
    assert context.parse_iso_result is None


# ---------------------------------------------------------------------------
# Plan: formatting datetimes as ISO strings
# ---------------------------------------------------------------------------


@when("a datetime value is formatted as an ISO string")
def step_call_to_iso_datetime(context):
    """Format a datetime value as an ISO string."""
    context.to_iso_result = LifecyclePlanModel._to_iso(datetime(2025, 6, 1, 12, 0, 0))


@then("the formatted string should match ISO-8601 format")
def step_verify_to_iso_string(context):
    """Verify the formatted string matches ISO-8601 format."""
    assert context.to_iso_result == "2025-06-01T12:00:00"


@when("an absent datetime is formatted as an ISO string")
def step_call_to_iso_none(context):
    """Format an absent (None) datetime as an ISO string."""
    context.to_iso_result = LifecyclePlanModel._to_iso(None)


@then("the formatted value should be absent")
def step_verify_to_iso_none(context):
    """Verify formatting an absent datetime returns nothing."""
    assert context.to_iso_result is None


# ---------------------------------------------------------------------------
# Plan: loading a stored record in the strategize phase
# ---------------------------------------------------------------------------


@given("a plan record exists in the strategize phase with queued state")
def step_create_plan_model_strategize_queued(context):
    """Persist a plan record in the strategize phase with queued state."""
    action_model = _make_action_model()
    context.db_session.add(action_model)
    context.db_session.commit()

    context.plan_model = _make_plan_model(
        phase="strategize",
        state="queued",
        project_names='["proj-1", "proj-2"]',
        tags='["important"]',
    )
    context.db_session.add(context.plan_model)
    context.db_session.commit()


@when("the plan record is loaded as a domain object")
def step_call_to_domain_on_plan(context):
    """Load the persisted plan record as a domain object."""
    context.plan_domain = context.plan_model.to_domain()


@then("the loaded plan should preserve its identity")
def step_verify_plan_identity(context):
    """Verify the loaded plan retains its identifier and attempt."""
    assert context.plan_domain.identity.plan_id == VALID_ULID_1
    assert context.plan_domain.identity.attempt == 1


@then("the loaded plan processing state should be queued")
def step_verify_plan_processing_state_queued(context):
    """Verify the loaded plan has QUEUED processing state."""
    from cleveragents.domain.models.core.plan import ProcessingState

    assert context.plan_domain.processing_state == ProcessingState.QUEUED


@then("the loaded plan should preserve its description")
def step_verify_plan_description(context):
    """Verify the loaded plan retains its description."""
    assert context.plan_domain.description == "A test plan description"


@then("the loaded plan should preserve its timestamps")
def step_verify_plan_timestamps(context):
    """Verify the loaded plan retains its timestamps."""
    assert context.plan_domain.timestamps.created_at == datetime.fromisoformat(
        "2025-06-01T12:00:00"
    )
    assert context.plan_domain.timestamps.updated_at == datetime.fromisoformat(
        "2025-06-01T13:00:00"
    )


@then("the loaded plan should preserve its metadata")
def step_verify_plan_metadata(context):
    """Verify the loaded plan retains its metadata fields."""
    assert context.plan_domain.created_by == "test-user"
    assert context.plan_domain.reusable is True
    assert context.plan_domain.read_only is False


# ---------------------------------------------------------------------------
# Plan: loading a stored record in the strategize phase
# ---------------------------------------------------------------------------


@given("a plan record exists in the strategize phase with processing state")
def step_create_plan_model_strategize(context):
    """Persist a plan record in the strategize phase."""
    existing = (
        context.db_session.query(LifecycleActionModel)
        .filter_by(namespaced_name="local/test-action")
        .first()
    )
    if not existing:
        action_model = _make_action_model()
        context.db_session.add(action_model)
        context.db_session.commit()

    context.plan_model = _make_plan_model(
        plan_id=VALID_ULID_2,
        phase="strategize",
        state="processing",
    )
    context.db_session.add(context.plan_model)
    context.db_session.commit()


@then("the loaded plan should be in the strategize phase")
def step_verify_plan_strategize_phase(context):
    """Verify the loaded plan is in the strategize phase."""
    from cleveragents.domain.models.core.plan import PlanPhase

    assert context.plan_domain.phase == PlanPhase.STRATEGIZE


@then("the loaded plan processing state should be processing")
def step_verify_plan_processing_state(context):
    """Verify the loaded plan processing state is processing."""
    from cleveragents.domain.models.core.plan import ProcessingState

    assert context.plan_domain.state == ProcessingState.PROCESSING


# ---------------------------------------------------------------------------
# Plan: loading a record with all phase timestamps
# ---------------------------------------------------------------------------


@given("a plan record exists with all phase timestamps populated")
def step_create_plan_model_all_timestamps(context):
    """Persist a plan record with all phase timestamps filled."""
    existing = (
        context.db_session.query(LifecycleActionModel)
        .filter_by(namespaced_name="local/test-action")
        .first()
    )
    if not existing:
        action_model = _make_action_model()
        context.db_session.add(action_model)
        context.db_session.commit()

    context.plan_model = _make_plan_model(
        plan_id=VALID_ULID_3,
        phase="apply",
        state="applied",
        strategize_started_at="2025-06-01T14:00:00",
        strategize_completed_at="2025-06-01T14:30:00",
        execute_started_at="2025-06-01T15:00:00",
        execute_completed_at="2025-06-01T15:30:00",
        apply_started_at="2025-06-01T16:00:00",
        applied_at="2025-06-01T16:30:00",
    )
    context.db_session.add(context.plan_model)
    context.db_session.commit()


@then("the loaded plan timestamps should include the strategize window")
def step_verify_strategize_timestamps(context):
    """Verify the loaded plan has correct strategize timestamps."""
    ts = context.plan_domain.timestamps
    assert ts.strategize_started_at == datetime(2025, 6, 1, 14, 0, 0)
    assert ts.strategize_completed_at == datetime(2025, 6, 1, 14, 30, 0)


@then("the loaded plan timestamps should include the execute window")
def step_verify_execute_timestamps(context):
    """Verify the loaded plan has correct execute timestamps."""
    ts = context.plan_domain.timestamps
    assert ts.execute_started_at == datetime(2025, 6, 1, 15, 0, 0)
    assert ts.execute_completed_at == datetime(2025, 6, 1, 15, 30, 0)


@then("the loaded plan timestamps should include the apply window")
def step_verify_apply_timestamps(context):
    """Verify the loaded plan has correct apply timestamps."""
    ts = context.plan_domain.timestamps
    assert ts.apply_started_at == datetime(2025, 6, 1, 16, 0, 0)
    assert ts.applied_at == datetime(2025, 6, 1, 16, 30, 0)


# ---------------------------------------------------------------------------
# Plan: loading a record with project IDs and tags
# ---------------------------------------------------------------------------


@given("a plan record exists with associated project identifiers and tags")
def step_create_plan_model_with_ids_tags(context):
    """Persist a plan record with project identifiers and tags."""
    existing = (
        context.db_session.query(LifecycleActionModel)
        .filter_by(namespaced_name="local/test-action")
        .first()
    )
    if not existing:
        action_model = _make_action_model()
        context.db_session.add(action_model)
        context.db_session.commit()

    context.plan_model = _make_plan_model(
        plan_id=VALID_ULID_4,
        phase="strategize",
        state="queued",
        project_names='["proj-a", "proj-b", "proj-c"]',
        tags='["urgent", "backend"]',
    )
    context.db_session.add(context.plan_model)
    context.db_session.commit()


@then("the loaded plan should contain the expected project identifiers")
def step_verify_plan_project_ids(context):
    """Verify the loaded plan contains the expected project identifiers."""
    project_names = [link.project_name for link in context.plan_domain.project_links]
    assert project_names == ["proj-a", "proj-b", "proj-c"]


@then("the loaded plan should contain the expected tags")
def step_verify_plan_tags(context):
    """Verify the loaded plan contains the expected tags."""
    assert context.plan_domain.tags == ["urgent", "backend"]


# ---------------------------------------------------------------------------
# Plan: storing a domain object with processing state
# ---------------------------------------------------------------------------


def _make_plan_domain(
    phase="strategize",
    processing_state=None,
    plan_id=VALID_ULID_1,
    project_links=None,
    tags=None,
    timestamps=None,
):
    """Create a Plan domain object for testing storage."""
    from cleveragents.domain.models.core.plan import (
        NamespacedName,
        Plan,
        PlanIdentity,
        PlanPhase,
        PlanTimestamps,
        ProcessingState,
        ProjectLink,
    )

    if project_links is None:
        project_links = [ProjectLink(project_name="proj-1")]
    if tags is None:
        tags = ["test"]
    if timestamps is None:
        timestamps = PlanTimestamps(
            created_at=datetime(2025, 6, 1, 12, 0, 0),
            updated_at=datetime(2025, 6, 1, 13, 0, 0),
        )

    resolved_processing = (
        processing_state if processing_state is not None else ProcessingState.QUEUED
    )

    return Plan(
        identity=PlanIdentity(plan_id=plan_id),
        namespaced_name=NamespacedName(namespace="local", name="test-plan"),
        action_name="local/test-action",
        description="Test plan description",
        definition_of_done="Tests pass",
        phase=PlanPhase(phase),
        processing_state=resolved_processing,
        strategy_actor="local/strategy",
        execution_actor="local/executor",
        project_links=project_links,
        timestamps=timestamps,
        created_by="test-user",
        tags=tags,
        reusable=True,
        read_only=False,
        effective_profile_snapshot="{}",
    )


@given("a plan domain object is prepared in the action phase with an available state")
def step_create_plan_domain_action_available(context):
    """Prepare a plan domain object in the strategize phase with queued state."""
    from cleveragents.domain.models.core.plan import ProcessingState, ProjectLink

    context.plan_domain_input = _make_plan_domain(
        phase="strategize",
        processing_state=ProcessingState.QUEUED,
        project_links=[
            ProjectLink(project_name="proj-1"),
            ProjectLink(project_name="proj-2"),
        ],
        tags=["ci", "test"],
    )


@when("the plan domain object is stored as a database record")
def step_call_from_domain_on_plan(context):
    """Store the plan domain object as a database record."""
    context.plan_model_result = LifecyclePlanModel.from_domain(
        context.plan_domain_input
    )


@then('the stored plan record should have state "{expected_state}"')
def step_verify_from_domain_plan_state(context, expected_state):
    """Verify the stored plan record has the expected processing_state."""
    assert context.plan_model_result.processing_state == expected_state


@then("the stored plan record should preserve the plan identifier")
def step_verify_from_domain_plan_id(context):
    """Verify the stored plan record retains the plan identifier."""
    assert context.plan_model_result.plan_id == VALID_ULID_1


@then("the stored plan record should preserve the phase")
def step_verify_from_domain_plan_phase(context):
    """Verify the stored plan record retains the phase."""
    assert context.plan_model_result.phase == "strategize"


@then("the stored plan record should serialize the project identifiers as JSON")
def step_verify_from_domain_project_ids(context):
    """Verify the stored plan record has project links via child table."""
    project_names = [
        pl.project_name for pl in context.plan_model_result.project_links_rel
    ]
    assert project_names == ["proj-1", "proj-2"]


@then("the stored plan record should serialize the plan tags as JSON")
def step_verify_from_domain_plan_tags(context):
    """Verify the stored plan record has tags serialized as JSON."""
    parsed = json.loads(context.plan_model_result.tags_json)
    assert parsed == ["ci", "test"]


@then("the stored plan record should format plan timestamps as ISO strings")
def step_verify_from_domain_plan_timestamps(context):
    """Verify the stored plan record has timestamps as ISO strings."""
    assert context.plan_model_result.created_at == "2025-06-01T12:00:00"
    assert context.plan_model_result.updated_at == "2025-06-01T13:00:00"


# ---------------------------------------------------------------------------
# Plan: storing a domain object with processing state
# ---------------------------------------------------------------------------


@given("a plan domain object is prepared in the strategize phase with a queued state")
def step_create_plan_domain_strategize_queued(context):
    """Prepare a plan domain object in the strategize phase with queued state."""
    from cleveragents.domain.models.core.plan import ProcessingState, ProjectLink

    context.plan_domain_input = _make_plan_domain(
        phase="strategize",
        processing_state=ProcessingState.QUEUED,
        project_links=[
            ProjectLink(project_name="proj-1"),
            ProjectLink(project_name="proj-2"),
        ],
        tags=["ci", "test"],
    )


@then('the stored plan record phase should be "{expected_phase}"')
def step_verify_from_domain_plan_phase_value(context, expected_phase):
    """Verify the stored plan record has the expected phase."""
    assert context.plan_model_result.phase == expected_phase


# ---------------------------------------------------------------------------
# Plan: storing a domain object with no explicit state
# ---------------------------------------------------------------------------


@given("a plan domain object is prepared with neither action nor processing state")
def step_create_plan_domain_null_states(context):
    """Prepare a plan-like object with no explicit state to test the fallback."""
    from types import SimpleNamespace

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

    context.plan_domain_stateless = SimpleNamespace(
        identity=PlanIdentity(plan_id=VALID_ULID_1),
        namespaced_name=NamespacedName(namespace="local", name="null-plan"),
        action_name="local/test-action",
        description="Null state plan",
        definition_of_done="Done",
        phase=PlanPhase.STRATEGIZE,
        processing_state=None,
        automation_profile=None,
        strategy_actor="local/strategy",
        execution_actor="local/executor",
        review_actor=None,
        apply_actor=None,
        estimation_actor=None,
        invariant_actor=None,
        execution_environment=None,
        execution_env_priority=None,
        project_links=[],
        invariants=[],
        arguments={},
        arguments_order=[],
        changeset_id=None,
        sandbox_refs=[],
        validation_summary=None,
        decision_root_id=None,
        timestamps=PlanTimestamps(
            created_at=datetime(2025, 6, 1, 12, 0, 0),
            updated_at=datetime(2025, 6, 1, 13, 0, 0),
        ),
        error_message=None,
        created_by=None,
        tags=[],
        reusable=True,
        read_only=False,
        effective_profile_snapshot="{}",
    )


@when("the stateless plan domain object is stored as a database record")
def step_call_from_domain_stateless(context):
    """Store the stateless plan as a database record."""
    context.plan_model_result = LifecyclePlanModel.from_domain(
        context.plan_domain_stateless
    )


# ---------------------------------------------------------------------------
# Plan: storing a domain object with all phase timestamps
# ---------------------------------------------------------------------------


@given("a plan domain object is prepared with all phase timestamps")
def step_create_plan_domain_all_timestamps(context):
    """Prepare a plan domain object with all phase timestamps."""
    from cleveragents.domain.models.core.plan import PlanTimestamps

    timestamps = PlanTimestamps(
        created_at=datetime(2025, 6, 1, 12, 0, 0),
        updated_at=datetime(2025, 6, 1, 13, 0, 0),
        strategize_started_at=datetime(2025, 6, 1, 14, 0, 0),
        strategize_completed_at=datetime(2025, 6, 1, 14, 30, 0),
        execute_started_at=datetime(2025, 6, 1, 15, 0, 0),
        execute_completed_at=datetime(2025, 6, 1, 15, 30, 0),
        apply_started_at=datetime(2025, 6, 1, 16, 0, 0),
        applied_at=datetime(2025, 6, 1, 16, 30, 0),
    )

    context.plan_domain_input = _make_plan_domain(
        phase="strategize",
        timestamps=timestamps,
    )


@then("the stored plan record should have all phase timestamps as ISO strings")
def step_verify_from_domain_all_timestamps(context):
    """Verify all phase timestamp columns are formatted as ISO strings."""
    m = context.plan_model_result
    assert m.strategize_started_at == "2025-06-01T14:00:00"
    assert m.strategize_completed_at == "2025-06-01T14:30:00"
    assert m.execute_started_at == "2025-06-01T15:00:00"
    assert m.execute_completed_at == "2025-06-01T15:30:00"
    assert m.apply_started_at == "2025-06-01T16:00:00"
    assert m.applied_at == "2025-06-01T16:30:00"
    assert m.completed_at == "2025-06-01T16:30:00"


# ---------------------------------------------------------------------------
# Plan: storing a domain object with non-default automation level
# ---------------------------------------------------------------------------


@given('a plan domain object is prepared with automation level "{level}"')
def step_create_plan_domain_with_automation_level(context, level):
    """Prepare a plan domain object (automation_level removed, uses profile)."""
    context.plan_domain_input = _make_plan_domain(
        phase="strategize",
    )


@then('the stored plan record automation level should be "{expected_level}"')
def step_verify_from_domain_automation_level(context, expected_level):
    """Verify the stored plan record (automation_level column removed)."""
    # automation_level column was removed; this step is a no-op
    pass


# ---------------------------------------------------------------------------
# Plan: storing a domain object with an explicit action_id
# ---------------------------------------------------------------------------

VALID_ULID_ACTION = "01JACTION000000000000ACTION"


@when("the plan domain object is stored with an explicit action identifier")
def step_call_from_domain_with_action_id(context):
    """Store the plan domain object with an explicit action_name."""
    context.plan_model_result = LifecyclePlanModel.from_domain(
        context.plan_domain_input,
        action_name=VALID_ULID_ACTION,
    )


@then("the stored plan record should preserve the supplied action identifier")
def step_verify_from_domain_action_id(context):
    """Verify the stored plan record retains the supplied action_id."""
    assert (
        context.plan_model_result.action_name == VALID_ULID_ACTION
    )  # still passed explicitly


# ---------------------------------------------------------------------------
# Round-trip persistence tests
# ---------------------------------------------------------------------------


@when("the action is saved to the database and reloaded as a domain object")
def step_convert_and_persist_action(context):
    """Save the action to the database and reload it as a domain object."""
    context.action_model_persisted = LifecycleActionModel.from_domain(
        context.action_domain_input
    )
    context.db_session.add(context.action_model_persisted)
    context.db_session.commit()

    context.action_model_retrieved = (
        context.db_session.query(LifecycleActionModel)
        .filter_by(namespaced_name=context.action_model_persisted.namespaced_name)
        .first()
    )
    assert context.action_model_retrieved is not None
    context.action_round_tripped = context.action_model_retrieved.to_domain()


@then("the reloaded action should match the original action")
def step_verify_round_trip_action(context):
    """Verify the reloaded action matches the original."""
    original = context.action_domain_input
    result = context.action_round_tripped
    assert str(result.namespaced_name) == str(original.namespaced_name)
    assert result.definition_of_done == original.definition_of_done
    assert result.strategy_actor == original.strategy_actor
    assert result.execution_actor == original.execution_actor
    assert result.reusable == original.reusable
    assert result.read_only == original.read_only
    assert result.tags == original.tags
    assert len(result.arguments) == len(original.arguments)


@when("the plan record is saved and then reloaded as a domain object")
def step_persist_and_reload_plan(context):
    """Save the plan record and reload it as a domain object."""
    # Plan was already persisted in the Given step
    context.plan_model_retrieved = (
        context.db_session.query(LifecyclePlanModel)
        .filter_by(plan_id=context.plan_model.plan_id)
        .first()
    )
    assert context.plan_model_retrieved is not None
    context.plan_round_tripped = context.plan_model_retrieved.to_domain()


@then("the reloaded plan should preserve its identity and phase")
def step_verify_round_trip_plan(context):
    """Verify the reloaded plan retains its identity and phase."""
    from cleveragents.domain.models.core.plan import PlanPhase

    result = context.plan_round_tripped
    assert result.identity.plan_id == VALID_ULID_1
    assert result.phase == PlanPhase.STRATEGIZE
    assert result.description == "A test plan description"
    assert result.created_by == "test-user"
