"""Tests for migration 0020_add_batch_txn_set_control_number.sql.

SP37 adds a nullable ``batches.transaction_set_control_number`` column
populated from the parsed 837's ST02 (transaction set control number)
on every write. The 999 ack join (Pass 1) needs to resolve by ST02,
not ISA13, so this column is the join key. This migration is purely
additive: nullable, no default, backfills from
``raw_result_json.envelope.transaction_set_control_number`` where the
source JSON already carries it.

For the backfill-shape tests we point ``db_migrate.MIGRATIONS_DIR``
at the real migrations directory, apply all migrations once to bring
the fresh DB up to v20, insert representative rows, then replay the
exact UPDATE statement the migration uses. Replaying the UPDATE
proves the SQL works as intended even though the migration itself
already ran over an empty table.

The backfill UPDATE is loaded directly from the migration file (not
a hand-maintained copy) so the test cannot drift from production —
see :func:`_load_migration_0020_backfill_sql` and the regression
locks in ``test_migration_0020_no_drift.py``.
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest
import sqlalchemy as sa

from cyclone import db_migrate


def _migration_0020_path() -> Path:
    return (
        Path(__file__).parent.parent / "src" / "cyclone" / "migrations"
        / "0020_add_batch_txn_set_control_number.sql"
    )


def _extract_update_statements(sql: str) -> list[str]:
    """Split a migration into statements; return the UPDATE ones.

    Mirrors db_migrate.run()'s splitter (strip ``--`` comments,
    split on ``;``) so the test extraction can never disagree with
    what the runner actually executes.
    """
    lines = [
        line for line in sql.splitlines()
        if not line.strip().startswith("--")
    ]
    cleaned = "\n".join(lines)
    return [
        stmt.strip() for stmt in cleaned.split(";")
        if stmt.strip()
    ]


def _load_migration_0020_backfill_sql() -> str:
    """Return migration 0020's UPDATE statement (the backfill).

    Reads the migration file at test time and extracts its UPDATE.
    Test code that needs to replay the backfill against rows that
    didn't exist when init_db ran uses this helper — guarantees the
    replayed SQL is byte-identical to what production will run.
    """
    sql = _migration_0020_path().read_text()
    updates = [
        s for s in _extract_update_statements(sql)
        if s.upper().startswith("UPDATE ")
    ]
    assert len(updates) == 1, (
        f"expected exactly 1 UPDATE in migration 0020, found {len(updates)}: {updates}"
    )
    return updates[0]


def _fresh_engine(path: Path) -> sa.Engine:
    return sa.create_engine(f"sqlite:///{path}", future=True)


def _table_info(engine: sa.Engine, table: str) -> list[tuple]:
    """Return PRAGMA table_info rows for ``table`` as plain tuples."""
    with engine.connect() as conn:
        return list(conn.exec_driver_sql(f"PRAGMA table_info({table});").tuples())


def _real_migrations_dir() -> Path:
    return Path(__file__).parent.parent / "src" / "cyclone" / "migrations"


@pytest.fixture
def migrated_engine(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
    """Yield an engine at v20 against which every real migration has run.

    Points ``db_migrate.MIGRATIONS_DIR`` at the real migrations
    directory so the test exercises the actual 0020 file, then runs
    the migration runner on a per-test fresh DB.
    """
    monkeypatch.setattr(db_migrate, "MIGRATIONS_DIR", _real_migrations_dir())
    engine = _fresh_engine(tmp_path / "mig0020.db")
    db_migrate.run(engine)

    # Confirm head is 22 (every migration applied, including SP41's 0022).
    with engine.connect() as conn:
        v = conn.exec_driver_sql("PRAGMA user_version").scalar() or 0
    assert v == 22, f"expected migration head=22, got {v}"

    yield engine
    engine.dispose()


def test_migration_0020_creates_column(migrated_engine) -> None:
    """Migration 0020 adds ``transaction_set_control_number`` to ``batches``."""
    cols = _table_info(migrated_engine, "batches")
    col_names = {row[1] for row in cols}
    assert "transaction_set_control_number" in col_names, (
        "batches.transaction_set_control_number missing — migration 0020 did not run. "
        f"Existing columns: {sorted(col_names)}"
    )


def test_migration_0020_column_is_nullable(migrated_engine) -> None:
    """The new column is nullable (no DEFAULT, no NOT NULL) so existing
    batches that don't yet carry the ST02 stay valid."""
    cols = {row[1]: row for row in _table_info(migrated_engine, "batches")}
    row = cols["transaction_set_control_number"]
    # PRAGMA table_info tuples: (cid, name, type, notnull, dflt_value, pk)
    assert row[3] == 0, f"notnull flag must be 0 (nullable), got {row[3]}"
    assert row[4] is None, f"dflt_value must be NULL, got {row[4]!r}"
    assert "TEXT" in (row[2] or "").upper(), f"expected TEXT column, got {row[2]!r}"


def test_migration_0020_backfills_when_key_present(
    migrated_engine: sa.Engine,
) -> None:
    """Replay the migration's backfill UPDATE against a row whose
    ``raw_result_json`` carries the key — must populate the new column.

    Replay (rather than waiting for ``db_migrate.run()`` to do it) is
    the only way to test the SQL against a row that didn't exist when
    init_db() ran; the migration's UPDATE naturally runs only over
    pre-existing rows.
    """
    st02_value = "ST0001"
    raw_json = {"envelope": {"transaction_set_control_number": st02_value}}

    with migrated_engine.begin() as conn:
        conn.exec_driver_sql(
            "INSERT INTO batches (id, kind, input_filename, parsed_at, raw_result_json) "
            "VALUES ('B-ST02-1', '837p', 'mig0020-st02.txt', '2026-07-07 00:00:00', ?)",
            (json.dumps(raw_json),),
        )
        conn.exec_driver_sql(_load_migration_0020_backfill_sql())

    with migrated_engine.connect() as conn:
        row = conn.exec_driver_sql(
            "SELECT transaction_set_control_number FROM batches WHERE id='B-ST02-1'"
        ).first()
    assert row is not None
    assert row[0] == st02_value, (
        f"backfill failed: expected {st02_value!r}, got {row[0]!r}"
    )


def test_migration_0020_backfill_conditional_on_key_present(
    migrated_engine: sa.Engine,
) -> None:
    """Replay the UPDATE against a row whose ``raw_result_json`` does NOT
    carry the key — must stay NULL. Proves the UPDATE is conditional
    (the ``json_extract(...) IS NOT NULL`` guard) rather than
    unconditionally overwriting with NULL."""
    raw_json = {"envelope": {"control_number": "ISA0001"}}  # no txn-set key
    with migrated_engine.begin() as conn:
        conn.exec_driver_sql(
            "INSERT INTO batches (id, kind, input_filename, parsed_at, raw_result_json) "
            "VALUES ('B-NOST-1', '837p', 'mig0020-nost.txt', '2026-07-07 00:00:00', ?)",
            (json.dumps(raw_json),),
        )
        conn.exec_driver_sql(_load_migration_0020_backfill_sql())

    with migrated_engine.connect() as conn:
        row = conn.exec_driver_sql(
            "SELECT transaction_set_control_number FROM batches WHERE id='B-NOST-1'"
        ).first()
    assert row is not None
    assert row[0] is None, (
        f"backfill must not overwrite when key is absent; got {row[0]!r}"
    )


def test_migration_0020_backfill_handles_null_raw_result_json(
    migrated_engine: sa.Engine,
) -> None:
    """A Batch row with ``raw_result_json IS NULL`` (the prior SP's
    unparsed state) must not crash the backfill and must stay NULL."""
    with migrated_engine.begin() as conn:
        conn.exec_driver_sql(
            "INSERT INTO batches (id, kind, input_filename, parsed_at) "
            "VALUES ('B-NULL-1', '837p', 'mig0020-null.txt', '2026-07-07 00:00:00')"
        )
        conn.exec_driver_sql(_load_migration_0020_backfill_sql())

    with migrated_engine.connect() as conn:
        row = conn.exec_driver_sql(
            "SELECT transaction_set_control_number FROM batches WHERE id='B-NULL-1'"
        ).first()
    assert row is not None
    assert row[0] is None
