"""
Tests for `RulesFileWriter` in scripts/calibrate_helpers.py.

Covers tasks 1.1/1.2 of the calibrate-assessment-inventory change: the
single canonical serialization path for `.dochygiene-rules.json` —
- canonical ordering: rules grouped delete-once-served -> temporary -> keep,
  glob-sorted within each group; nominations after rules; consults before
  rejected, each glob-sorted
- idempotent round-trip (byte-identical rewrite of an already-canonical file)
- hand-edit re-canonicalization (out-of-order input is rewritten canonical)
- unknown-field warn-and-round-trip, for both rules and nomination entries

sys.path is patched here (no shared conftest), matching the existing
tests/test_calibrate_helpers.py pattern.
"""

from __future__ import annotations

import json
import sys
from pathlib import Path

_SCRIPTS_DIR = Path(__file__).parent.parent / "scripts"
if str(_SCRIPTS_DIR) not in sys.path:
    sys.path.insert(0, str(_SCRIPTS_DIR))

from calibrate_helpers import RulesFileWriter  # noqa: E402


def _rule(glob, lifetime, **extra):
    d = {"glob": glob, "lifetime": lifetime, "confirmed_by": "human", "confirmed_on": "2026-07-15"}
    d.update(extra)
    return d


class TestCanonicalOrdering:
    def test_rules_grouped_by_tier_in_order_and_glob_sorted(self):
        data = {
            "schema_version": 1,
            "rules": [
                _rule("z/keep.md", "keep"),
                _rule("a/keep.md", "keep"),
                _rule("b/temp/*.md", "temporary"),
                _rule("a/temp/*.md", "temporary"),
                _rule("z/served.md", "delete-once-served"),
                _rule("a/served.md", "delete-once-served"),
            ],
        }
        writer = RulesFileWriter()
        canonical, _warnings = writer.canonicalize(data)
        globs = [r["glob"] for r in canonical["rules"]]
        assert globs == [
            "a/served.md",
            "z/served.md",
            "a/temp/*.md",
            "b/temp/*.md",
            "a/keep.md",
            "z/keep.md",
        ]

    def test_nominations_after_rules_consults_before_rejected_sorted(self):
        data = {
            "schema_version": 1,
            "rules": [_rule("a.md", "keep")],
            "nominations": {
                "rejected": [
                    {
                        "glob": "z/reject.md",
                        "lifetime": "temporary",
                        "why": "why z",
                        "rejected_by": "judge",
                        "judged_on": "2026-07-15",
                    },
                    {
                        "glob": "a/reject.md",
                        "lifetime": "temporary",
                        "why": "why a",
                        "rejected_by": "judge",
                        "judged_on": "2026-07-15",
                    },
                ],
                "consults": [
                    {
                        "glob": "z/consult.md",
                        "question": "q z",
                        "evidence": "e",
                        "cluster_key": "k",
                        "asked_on": "2026-07-15",
                    },
                    {
                        "glob": "a/consult.md",
                        "question": "q a",
                        "evidence": "e",
                        "cluster_key": "k",
                        "asked_on": "2026-07-15",
                    },
                ],
            },
        }
        writer = RulesFileWriter()
        canonical, _warnings = writer.canonicalize(data)

        keys = list(canonical.keys())
        assert keys.index("rules") < keys.index("nominations")

        nomination_keys = list(canonical["nominations"].keys())
        assert nomination_keys.index("consults") < nomination_keys.index("rejected")

        assert [c["glob"] for c in canonical["nominations"]["consults"]] == [
            "a/consult.md",
            "z/consult.md",
        ]
        assert [r["glob"] for r in canonical["nominations"]["rejected"]] == [
            "a/reject.md",
            "z/reject.md",
        ]

    def test_nominations_omitted_when_absent(self):
        data = {"schema_version": 1, "rules": [_rule("a.md", "keep")]}
        writer = RulesFileWriter()
        canonical, _warnings = writer.canonicalize(data)
        assert "nominations" not in canonical

    def test_nominations_omitted_when_present_but_empty(self):
        data = {
            "schema_version": 1,
            "rules": [_rule("a.md", "keep")],
            "nominations": {"consults": [], "rejected": []},
        }
        writer = RulesFileWriter()
        canonical, _warnings = writer.canonicalize(data)
        assert "nominations" not in canonical


class TestIdempotentRoundTrip:
    def test_writing_an_already_canonical_file_is_byte_identical(self, tmp_path):
        data = {
            "schema_version": 1,
            "rules": [
                _rule("a/served.md", "delete-once-served"),
                _rule("a/temp/*.md", "temporary"),
                _rule("a/keep.md", "keep"),
            ],
            "nominations": {
                "consults": [
                    {
                        "glob": "a/consult.md",
                        "question": "q",
                        "evidence": "e",
                        "cluster_key": "k",
                        "asked_on": "2026-07-15",
                    }
                ],
                "rejected": [
                    {
                        "glob": "a/reject.md",
                        "lifetime": "temporary",
                        "why": "why",
                        "rejected_by": "judge",
                        "judged_on": "2026-07-15",
                    }
                ],
            },
        }
        writer = RulesFileWriter()
        path = tmp_path / ".dochygiene-rules.json"
        writer.write(path, data)
        first_bytes = path.read_bytes()

        loaded, _warnings = writer.load(path)
        writer.write(path, loaded)
        second_bytes = path.read_bytes()

        assert first_bytes == second_bytes


class TestHandEditRecanonicalization:
    def test_hand_edited_out_of_order_file_recanonicalizes_on_next_write(self, tmp_path):
        # Hand-authored file: keep rule listed before temporary/served rules,
        # rejected before consults -- out of canonical order.
        hand_edited = {
            "schema_version": 1,
            "rules": [
                _rule("a/keep.md", "keep"),
                _rule("a/served.md", "delete-once-served"),
                _rule("a/temp/*.md", "temporary"),
            ],
            "nominations": {
                "rejected": [
                    {
                        "glob": "a/reject.md",
                        "lifetime": "temporary",
                        "why": "why",
                        "rejected_by": "judge",
                        "judged_on": "2026-07-15",
                    }
                ],
                "consults": [
                    {
                        "glob": "a/consult.md",
                        "question": "q",
                        "evidence": "e",
                        "cluster_key": "k",
                        "asked_on": "2026-07-15",
                    }
                ],
            },
        }
        path = tmp_path / ".dochygiene-rules.json"
        path.write_text(json.dumps(hand_edited, indent=2), encoding="utf-8")

        writer = RulesFileWriter()
        loaded, _warnings = writer.load(path)
        writer.write(path, loaded)

        on_disk = json.loads(path.read_text(encoding="utf-8"))
        assert [r["glob"] for r in on_disk["rules"]] == [
            "a/served.md",
            "a/temp/*.md",
            "a/keep.md",
        ]
        nomination_keys = list(on_disk["nominations"].keys())
        assert nomination_keys.index("consults") < nomination_keys.index("rejected")


class TestUnknownFieldsWarnAndRoundtrip:
    def test_unknown_rule_field_warns_and_is_preserved(self):
        data = {
            "schema_version": 1,
            "rules": [_rule("a.md", "keep", mystery_field="surprise")],
        }
        writer = RulesFileWriter()
        canonical, warnings = writer.canonicalize(data)
        assert canonical["rules"][0]["mystery_field"] == "surprise"
        assert any("mystery_field" in w for w in warnings)

    def test_unknown_consult_field_warns_and_is_preserved(self):
        data = {
            "schema_version": 1,
            "rules": [],
            "nominations": {
                "consults": [
                    {
                        "glob": "a/consult.md",
                        "question": "q",
                        "evidence": "e",
                        "cluster_key": "k",
                        "asked_on": "2026-07-15",
                        "extra_field": "unexpected",
                    }
                ],
                "rejected": [],
            },
        }
        writer = RulesFileWriter()
        canonical, warnings = writer.canonicalize(data)
        assert canonical["nominations"]["consults"][0]["extra_field"] == "unexpected"
        assert any("extra_field" in w for w in warnings)

    def test_unknown_rejected_field_warns_and_is_preserved(self):
        data = {
            "schema_version": 1,
            "rules": [],
            "nominations": {
                "consults": [],
                "rejected": [
                    {
                        "glob": "a/reject.md",
                        "lifetime": "temporary",
                        "why": "why",
                        "rejected_by": "judge",
                        "judged_on": "2026-07-15",
                        "surprise_field": "oops",
                    }
                ],
            },
        }
        writer = RulesFileWriter()
        canonical, warnings = writer.canonicalize(data)
        assert canonical["nominations"]["rejected"][0]["surprise_field"] == "oops"
        assert any("surprise_field" in w for w in warnings)


class TestLoad:
    def test_load_reads_file_and_returns_canonical_data(self, tmp_path):
        data = {
            "schema_version": 1,
            "rules": [_rule("a/keep.md", "keep")],
        }
        path = tmp_path / ".dochygiene-rules.json"
        path.write_text(json.dumps(data), encoding="utf-8")
        writer = RulesFileWriter()
        loaded, warnings = writer.load(path)
        assert loaded["rules"][0]["glob"] == "a/keep.md"
        assert warnings == []

    def test_load_missing_file_returns_empty_envelope(self, tmp_path):
        path = tmp_path / "does-not-exist.json"
        writer = RulesFileWriter()
        loaded, warnings = writer.load(path)
        assert loaded["rules"] == []
        assert "nominations" not in loaded
        assert warnings == []
