"""Step definitions for db_migration_lifecycle.feature — schema parity.

FK structure verification, orphan rejection, valid references, and
partial index checks for the ``resource_links``, ``checkpoint_metadata``,
and ``decisions`` tables.
"""

from __future__ import annotations

from typing import Any

from behave import then
from sqlalchemy import inspect as sa_inspect
from sqlalchemy import text
from sqlalchemy.exc import IntegrityError

# ---------------------------------------------------------------------------
# Shared test-data helpers
# ---------------------------------------------------------------------------


def _ensure_test_action_and_plan(conn: Any, action_name: str, plan_id: str) -> None:
    """Insert prerequisite action and plan rows if absent."""
    existing_action = conn.execute(
        text("SELECT 1 FROM actions WHERE namespaced_name = :n"),
        {"n": action_name},
    ).fetchone()
    existing_plan = conn.execute(
        text("SELECT 1 FROM v3_plans WHERE plan_id = :pid"),
        {"pid": plan_id},
    ).fetchone()
    if existing_action is not None and existing_plan is not None:
        return

    if existing_action is None:
        conn.execute(
            text(
                """
                INSERT INTO actions (
                    namespaced_name, namespace, name, description,
                    definition_of_done, strategy_actor, execution_actor,
                    created_at, updated_at
                ) VALUES (
                    :namespaced_name, :namespace, :name, :description,
                    :definition_of_done, :strategy_actor, :execution_actor,
                    :created_at, :updated_at
                )
                """
            ),
            {
                "namespaced_name": action_name,
                "namespace": "local",
                "name": action_name.split("/")[-1],
                "description": "test action",
                "definition_of_done": "test dod",
                "strategy_actor": "local/strategy",
                "execution_actor": "local/execution",
                "created_at": "2026-01-01T00:00:00",
                "updated_at": "2026-01-01T00:00:00",
            },
        )

    if existing_plan is None:
        conn.execute(
            text(
                """
                INSERT INTO v3_plans (
                    plan_id, root_plan_id, action_name,
                    namespaced_name, namespace,
                    description, created_at, updated_at
                ) VALUES (
                    :plan_id, :root_plan_id, :action_name,
                    :namespaced_name, :namespace,
                    :description, :created_at, :updated_at
                )
                """
            ),
            {
                "plan_id": plan_id,
                "root_plan_id": plan_id,
                "action_name": action_name,
                "namespaced_name": "local/test-plan-fk",
                "namespace": "local",
                "description": "test plan",
                "created_at": "2026-01-01T00:00:00",
                "updated_at": "2026-01-01T00:00:00",
            },
        )


def _insert_test_resource(
    conn: Any,
    resource_id: str,
    type_name: str = "git-checkout",
    resource_kind: str = "physical",
) -> None:
    """Insert a test resource row, handling the optional namespaced_name column."""
    resource_columns = {
        col["name"] for col in sa_inspect(conn).get_columns("resources")
    }
    cols = "resource_id, type_name, resource_kind, created_at, updated_at"
    vals = ":resource_id, :type_name, :resource_kind, :created_at, :updated_at"
    params: dict[str, str] = {
        "resource_id": resource_id,
        "type_name": type_name,
        "resource_kind": resource_kind,
        "created_at": "2026-01-01T00:00:00",
        "updated_at": "2026-01-01T00:00:00",
    }
    if "namespaced_name" in resource_columns:
        cols = (
            "resource_id, namespaced_name, type_name,"
            " resource_kind, created_at, updated_at"
        )
        vals = (
            ":resource_id, :namespaced_name, :type_name,"
            " :resource_kind, :created_at, :updated_at"
        )
        params["namespaced_name"] = f"local/{resource_id}"
    conn.execute(text(f"INSERT INTO resources ({cols}) VALUES ({vals})"), params)


# ---------------------------------------------------------------------------
# Then — link_type default verification
# ---------------------------------------------------------------------------


@then('the "resource_links" table should include "link_type" with default "contains"')
def step_resource_links_has_link_type_default(context: Any) -> None:
    inspector = sa_inspect(context.engine)
    columns = inspector.get_columns("resource_links")
    link_type = next(
        (column for column in columns if column["name"] == "link_type"), None
    )
    assert link_type is not None, "resource_links.link_type column is missing"

    default = str(link_type.get("default") or "").lower()
    assert "contains" in default, (
        "Expected resource_links.link_type default to include 'contains', "
        f"got: {link_type.get('default')!r}"
    )


# ---------------------------------------------------------------------------
# Then — checkpoint FK structure verification
# ---------------------------------------------------------------------------


@then("checkpoint_metadata should enforce decision and resource foreign keys")
def step_checkpoint_metadata_foreign_keys(context: Any) -> None:
    inspector = sa_inspect(context.engine)
    foreign_keys = inspector.get_foreign_keys("checkpoint_metadata")
    signatures = {
        (
            tuple(fk.get("constrained_columns") or []),
            fk.get("referred_table"),
            tuple(fk.get("referred_columns") or []),
        )
        for fk in foreign_keys
    }

    decision_fk = (("decision_id",), "decisions", ("decision_id",))
    resource_fk = (("resource_id",), "resources", ("resource_id",))

    assert decision_fk in signatures, (
        "Missing checkpoint_metadata foreign key for decision_id -> "
        "decisions.decision_id"
    )
    assert resource_fk in signatures, (
        "Missing checkpoint_metadata foreign key for resource_id -> "
        "resources.resource_id"
    )


# ---------------------------------------------------------------------------
# Then — orphan FK rejection (combined)
# ---------------------------------------------------------------------------


@then("checkpoint_metadata foreign keys should reject orphan references")
def step_checkpoint_metadata_foreign_keys_reject_orphans(context: Any) -> None:
    action_name = "local/test-action"
    plan_id = "01ARZ3NDEKTSV4RRFFQ69G5FAV"

    with context.engine.begin() as conn:
        _ensure_test_action_and_plan(conn, action_name, plan_id)

        try:
            conn.execute(
                text(
                    """
                    INSERT INTO checkpoint_metadata (
                        checkpoint_id,
                        plan_id,
                        decision_id,
                        checkpoint_type,
                        resource_id,
                        sandbox_ref,
                        filesystem_path,
                        created_at
                    ) VALUES (
                        :checkpoint_id,
                        :plan_id,
                        :decision_id,
                        :checkpoint_type,
                        :resource_id,
                        :sandbox_ref,
                        :filesystem_path,
                        :created_at
                    )
                    """
                ),
                {
                    "checkpoint_id": "01ARZ3NDEKTSV4RRFFQ69G5FAW",
                    "plan_id": plan_id,
                    "decision_id": "01ARZ3NDEKTSV4RRFFQ69G5FAX",
                    "checkpoint_type": "manual",
                    "resource_id": "01ARZ3NDEKTSV4RRFFQ69G5FAY",
                    "sandbox_ref": "test-ref",
                    "filesystem_path": "",
                    "created_at": "2026-01-01T00:00:00",
                },
            )
        except IntegrityError:
            return

    raise AssertionError(
        "checkpoint_metadata accepted orphan decision/resource references"
    )


# ---------------------------------------------------------------------------
# Then — independent orphan FK rejection
# ---------------------------------------------------------------------------


@then("checkpoint_metadata should reject orphan decision_id independently")
def step_checkpoint_reject_orphan_decision_only(context: Any) -> None:
    action_name = "local/test-action-fk-d"
    plan_id = "01ARZ3NDEKTSV4RRFFQ69G5FB0"

    with context.engine.begin() as conn:
        _ensure_test_action_and_plan(conn, action_name, plan_id)

        try:
            conn.execute(
                text(
                    """
                    INSERT INTO checkpoint_metadata (
                        checkpoint_id, plan_id, decision_id,
                        checkpoint_type, sandbox_ref,
                        filesystem_path, created_at
                    ) VALUES (
                        :checkpoint_id, :plan_id, :decision_id,
                        :checkpoint_type, :sandbox_ref,
                        :filesystem_path, :created_at
                    )
                    """
                ),
                {
                    "checkpoint_id": "01ARZ3NDEKTSV4RRFFQ69G5FB1",
                    "plan_id": plan_id,
                    "decision_id": "01ARZ3NDEKTSV4RRFFQ69G5FB2",
                    "checkpoint_type": "manual",
                    "sandbox_ref": "test-ref",
                    "filesystem_path": "",
                    "created_at": "2026-01-01T00:00:00",
                },
            )
        except IntegrityError:
            return

    raise AssertionError(
        "checkpoint_metadata accepted orphan decision_id with NULL resource_id"
    )


@then("checkpoint_metadata should reject orphan resource_id independently")
def step_checkpoint_reject_orphan_resource_only(context: Any) -> None:
    action_name = "local/test-action-fk-r"
    plan_id = "01ARZ3NDEKTSV4RRFFQ69G5FB3"

    with context.engine.begin() as conn:
        _ensure_test_action_and_plan(conn, action_name, plan_id)

        try:
            conn.execute(
                text(
                    """
                    INSERT INTO checkpoint_metadata (
                        checkpoint_id, plan_id, resource_id,
                        checkpoint_type, sandbox_ref,
                        filesystem_path, created_at
                    ) VALUES (
                        :checkpoint_id, :plan_id, :resource_id,
                        :checkpoint_type, :sandbox_ref,
                        :filesystem_path, :created_at
                    )
                    """
                ),
                {
                    "checkpoint_id": "01ARZ3NDEKTSV4RRFFQ69G5FB4",
                    "plan_id": plan_id,
                    "resource_id": "01ARZ3NDEKTSV4RRFFQ69G5FB5",
                    "checkpoint_type": "manual",
                    "sandbox_ref": "test-ref",
                    "filesystem_path": "",
                    "created_at": "2026-01-01T00:00:00",
                },
            )
        except IntegrityError:
            return

    raise AssertionError(
        "checkpoint_metadata accepted orphan resource_id with NULL decision_id"
    )


# ---------------------------------------------------------------------------
# Then — valid FK acceptance
# ---------------------------------------------------------------------------


@then("checkpoint_metadata should accept valid decision and resource references")
def step_checkpoint_accept_valid_references(context: Any) -> None:
    action_name = "local/test-action-fk-v"
    plan_id = "01ARZ3NDEKTSV4RRFFQ69G5FB6"
    decision_id = "01ARZ3NDEKTSV4RRFFQ69G5FB7"
    resource_id = "01ARZ3NDEKTSV4RRFFQ69G5FB8"

    with context.engine.begin() as conn:
        _ensure_test_action_and_plan(conn, action_name, plan_id)

        conn.execute(
            text(
                """
                INSERT INTO decisions (
                    decision_id, plan_id, decision_type, question,
                    chosen_option, context_snapshot_json, sequence_number,
                    created_at
                ) VALUES (
                    :decision_id, :plan_id, :decision_type, :question,
                    :chosen_option, :context_snapshot_json, :sequence_number,
                    :created_at
                )
                """
            ),
            {
                "decision_id": decision_id,
                "plan_id": plan_id,
                "decision_type": "strategy_choice",
                "question": "test question",
                "chosen_option": "test option",
                "context_snapshot_json": "{}",
                "sequence_number": 1,
                "created_at": "2026-01-01T00:00:00",
            },
        )

        _insert_test_resource(conn, resource_id)

        conn.execute(
            text(
                """
                INSERT INTO checkpoint_metadata (
                    checkpoint_id, plan_id, decision_id,
                    checkpoint_type, resource_id, sandbox_ref,
                    filesystem_path, created_at
                ) VALUES (
                    :checkpoint_id, :plan_id, :decision_id,
                    :checkpoint_type, :resource_id, :sandbox_ref,
                    :filesystem_path, :created_at
                )
                """
            ),
            {
                "checkpoint_id": "01ARZ3NDEKTSV4RRFFQ69G5FB9",
                "plan_id": plan_id,
                "decision_id": decision_id,
                "checkpoint_type": "manual",
                "resource_id": resource_id,
                "sandbox_ref": "test-ref",
                "filesystem_path": "",
                "created_at": "2026-01-01T00:00:00",
            },
        )


# ---------------------------------------------------------------------------
# Then — partial index verification
# ---------------------------------------------------------------------------


@then(
    'the "decisions" table should have partial index "{index_name}" on "{column_name}"'
)
def step_decisions_has_partial_index(
    context: Any,
    index_name: str,
    column_name: str,
) -> None:
    inspector = sa_inspect(context.engine)
    indexes = inspector.get_indexes("decisions")
    index = next((idx for idx in indexes if idx.get("name") == index_name), None)
    assert index is not None, f"Index {index_name} not found on decisions"

    columns = list(index.get("column_names") or [])
    assert columns == [column_name], (
        f"Index {index_name} expected on [{column_name!r}], got {columns!r}"
    )

    # Verify partial WHERE clause via sqlite_master (SQLite-only).
    if context.engine.dialect.name == "sqlite":
        with context.engine.connect() as conn:
            row = conn.execute(
                text(
                    "SELECT sql FROM sqlite_master "
                    "WHERE type = 'index' AND name = :index_name"
                ),
                {"index_name": index_name},
            ).fetchone()

        assert row is not None, f"sqlite_master entry missing for index {index_name}"
        sql = str(row[0] or "").lower()
        assert "where superseded_by is not null" in sql, (
            f"Expected partial WHERE clause on {index_name}, got SQL: {row[0]!r}"
        )
