"""Equivalence harness for the #610 one-file-per-record migration.

The migration is a MOVE: content relocates, nothing is rewritten. That makes correctness
*provable* rather than reviewable — parse the corpus before, run the migration, parse it after,
and assert the two Record sets are identical field-for-field with byte-identical rationale.

**Runs against a checked-in FIXTURE, not the live corpus.** An earlier version migrated the real
`docs/` tree, which made it a one-shot: the moment the real migration landed, the tree was already
split, the harness had nothing to move, and the tests failed for a reason that had nothing to do
with the code under test. A fixture keeps these tests exercising the *script* rather than the
repo's current state, so they stay meaningful and green after the migration ships.

The one-off proof against the real corpus was run separately at migration time: 168 records before
and after, zero lost, zero gained, zero field mismatches, zero rationale bodies differing.
"""

from __future__ import annotations

import shutil
from pathlib import Path

import pytest

import scripts.decisions_lib as dl
import scripts.migrate_decisions_split as mig


def _independent_strip(rec: dl.Record) -> str:
    """Strip the metadata block WITHOUT reusing the migration's own helper.

    Deliberately a second implementation. Computing the "before" baseline with
    `mig._rationale_body` made the byte-identity proof circular: a bug in that one function
    corrupts both sides equally and the assertion still passes. Review proved it — collapsing
    interior blank lines there left all harness tests green.
    """
    lines = rec.body.split("\n")
    while lines and not lines[0].strip():
        lines.pop(0)
    if lines and lines[0].lstrip().startswith("`key:"):
        lines.pop(0)
        while lines and lines[0].strip():
            lines.pop(0)
    return "\n".join(lines).strip()


FIXTURE = Path(__file__).parent / "fixtures" / "premigration"

COMPARED_FIELDS = (
    "key",
    "status",
    "since",
    "supersedes",
    "superseded_by",
    "stale_after",
    "rule",
    "signals",
    "mechanics",
    "sources",
)

# The fixture is static, so an exact count is a real completeness guard here rather than a number
# that rots every time a record lands on main.
EXPECTED_RECORDS = 4


@pytest.fixture(scope="module")
def migrated(tmp_path_factory):
    """Copy the fixture corpus to tmp, point decisions_lib at it, run the real migration."""
    tmp = tmp_path_factory.mktemp("corpus")
    shutil.copytree(FIXTURE, tmp / "docs")

    orig = (dl.REPO_ROOT, dl.DECISIONS_MD, dl.TOPIC_DIR, dl.ARCHIVE_DIR, dl.RECORDS_DIR)
    dl.REPO_ROOT = tmp
    dl.DECISIONS_MD = tmp / "docs" / "decisions.md"
    dl.TOPIC_DIR = tmp / "docs" / "decisions"
    dl.ARCHIVE_DIR = dl.TOPIC_DIR / "archive"
    dl.RECORDS_DIR = dl.TOPIC_DIR / "records"
    try:
        before = {}
        for f in [dl.DECISIONS_MD, *sorted(dl.TOPIC_DIR.glob("*.md")), *sorted(dl.ARCHIVE_DIR.rglob("*.md"))]:
            for r in dl.parse_file(f):
                if r.key and r.heading not in dl.SKIP_HEADINGS:
                    before[r.key] = (r, _independent_strip(r))

        mig.main([])

        after = {}
        for f in [*dl.RECORDS_DIR.rglob("*.md"), *dl.ARCHIVE_DIR.rglob("*.md")]:
            for r in dl.parse_file(f):
                if r.key:
                    after[r.key] = (r, r.body)
        yield before, after, tmp
    finally:
        dl.REPO_ROOT, dl.DECISIONS_MD, dl.TOPIC_DIR, dl.ARCHIVE_DIR, dl.RECORDS_DIR = orig


def test_fixture_is_actually_pre_migration(migrated):
    """Guard against the harness silently testing nothing if the fixture ever gets migrated."""
    before, _, _ = migrated
    assert len(before) == EXPECTED_RECORDS, f"fixture parsed {len(before)}, expected {EXPECTED_RECORDS}"


def test_every_record_survives_with_the_same_key(migrated):
    before, after, _ = migrated
    assert set(before) == set(after), (
        f"lost: {sorted(set(before) - set(after))} / gained: {sorted(set(after) - set(before))}"
    )


def test_every_metadata_field_round_trips(migrated):
    before, after, _ = migrated
    diffs = []
    for key, (b, _) in before.items():
        a, _ = after[key]
        for f in COMPARED_FIELDS:
            if getattr(b, f) != getattr(a, f):
                diffs.append(f"{key}.{f}: {getattr(b, f)!r} -> {getattr(a, f)!r}")
    assert not diffs, "\n".join(diffs)


def test_title_round_trips_from_the_heading(migrated):
    before, after, _ = migrated
    diffs = [
        f"{k}: {b.heading!r} -> {after[k][0].heading!r}"
        for k, (b, _) in before.items()
        if b.heading != after[k][0].heading
    ]
    assert not diffs, "\n".join(diffs)


def test_rationale_bodies_are_byte_identical(migrated):
    """Including interior blank lines and backticks — the bytes are the payload."""
    before, after, _ = migrated
    diffs = [k for k, (_, body) in before.items() if body != after[k][1]]
    assert not diffs, f"{len(diffs)} rationale bodies changed: {diffs}"


def test_path_matches_key(migrated):
    _, after, _tmp = migrated
    bad = []
    for key, (rec, _) in after.items():
        area, _, topic = key.partition(".")
        if rec.source.parent.name != area or rec.source.stem != topic:
            bad.append(f"{key} -> {rec.source}")
    assert not bad, "\n".join(bad)


def test_archived_records_land_in_the_archive_wing(migrated):
    _, after, _tmp = migrated
    misfiled = [
        key
        for key, (rec, _) in after.items()
        if (rec.status in ("superseded", "retired")) != ("archive" in rec.source.parts)
    ]
    assert not misfiled, misfiled


def test_source_files_are_kept_and_retain_their_narrative(migrated):
    """Preamble prose is often the only copy — it must survive, not be deleted with the records."""
    _, _, tmp = migrated
    kept = tmp / "docs" / "decisions.md"
    assert kept.exists(), "decisions.md was deleted"
    assert "the only copy of it" in kept.read_text()
    topic = tmp / "docs" / "decisions" / "topic.md"
    assert topic.exists() and "the only copy of this narrative" in topic.read_text()


def test_no_records_remain_in_the_legacy_files(migrated):
    _, _, tmp = migrated
    leftovers = {}
    for f in [tmp / "docs" / "decisions.md", *sorted((tmp / "docs" / "decisions").glob("*.md"))]:
        if f.name in dl._NON_DECISION_FILES:
            continue
        keyed = [r.key for r in dl.parse_file(f) if r.key]
        if keyed:
            leftovers[f.name] = keyed
    assert not leftovers, leftovers


def test_same_file_anchor_bullets_are_dropped(migrated):
    """The split makes them WRONG, not merely stale — they point at headings that left the file."""
    _, _, tmp = migrated
    assert "](#" not in (tmp / "docs" / "decisions.md").read_text(), "a same-file anchor now dangles"


def test_every_generated_link_resolves_from_its_own_file(migrated):
    """Resolve each link relative to the file it LIVES IN — the base the reader's browser uses.

    The earlier version of this test asserted existence from `docs/decisions/` regardless of where
    the link sat, which is the wrong base for `docs/decisions.md` (parent `docs/`) and for the
    archive files (parent `docs/decisions/archive/`). It therefore encoded the very bug it was
    meant to catch: 105 links dangled while it stayed green.
    """
    _, _, tmp = migrated
    import re as _re

    dangling = []
    checked = 0
    for f in tmp.rglob("*.md"):
        if mig._MOVED_HEADING not in f.read_text():
            continue
        section = f.read_text().split(mig._MOVED_HEADING, 1)[1]
        for link in _re.findall(r"\]\(([^)#]+)\)", section):
            checked += 1
            if not (f.parent / link).resolve().exists():
                dangling.append(f"{f.relative_to(tmp)} -> {link}")
    assert checked, "no generated links found — test would pass vacuously"
    assert not dangling, f"{len(dangling)} dangling of {checked}:\n" + "\n".join(dangling[:10])
