from __future__ import annotations

from copy import deepcopy
from pathlib import Path

from crowley_diary.builder import build_candidate_page
from crowley_diary.schema_tools import export_model_schema, load_canonical_schema, schema_errors
from crowley_diary.source import SourcePackage
from crowley_diary.review import stage_agent_review
from crowley_diary.util import load_json, project_root, write_json, write_text
from crowley_diary.validation import validate_page


def test_model_schema_accepts_blank_fixture() -> None:
    fixture = load_json(project_root() / "tests" / "fixtures" / "model" / "page-039.json")
    assert schema_errors(fixture, export_model_schema()) == []


def test_model_schema_uses_provider_supported_projection() -> None:
    schema = export_model_schema()
    rendered = str(schema)
    assert "'oneOf'" not in rendered
    assert "'allOf'" not in rendered
    assert "'not'" not in rendered
    assert "'uniqueItems'" not in rendered
    assert list(schema["$defs"]) == sorted(schema["$defs"])


def test_model_schema_rejects_unknown_page_role() -> None:
    fixture = load_json(project_root() / "tests" / "fixtures" / "model" / "page-039.json")
    fixture["page_role"] = "modernized"
    assert schema_errors(fixture, export_model_schema())


def test_normalized_date_fixture_is_schema_valid() -> None:
    fixture = load_json(project_root() / "tests" / "fixtures" / "model" / "page-181.json")
    assert schema_errors(fixture, export_model_schema()) == []


def test_gap_range_reuses_source_date_and_allows_null_endpoint(
    source_root: Path,
) -> None:
    source = SourcePackage(source_root)
    page = load_json(project_root() / "evaluation" / "goldens" / "page-110.json")
    block = page["blocks"][0]
    assert block["start_date"]["date_source"] == "July 25, 1897"
    assert block["end_date"] == {
        "date_source": "Nov. 28.",
        "date_normalized": "1897-11-28",
        "normalization_status": "inferred",
        "normalization_confidence": "high",
        "normalization_reason": (
            "The visible endpoint omits its year; 1897 is inferred from the "
            "explicitly dated start of the same source range."
        ),
    }
    assert validate_page(page, source).valid

    missing_end = deepcopy(page)
    missing_end["blocks"][0]["end_date"] = None
    assert validate_page(missing_end, source).valid


def test_gap_endpoint_cannot_silently_supply_missing_source_year(
    source_root: Path,
) -> None:
    source = SourcePackage(source_root)
    page = load_json(project_root() / "evaluation" / "goldens" / "page-110.json")
    page["blocks"][0]["end_date"]["normalization_status"] = "normalized"
    report = validate_page(page, source)
    assert not report.valid
    assert any("year absent from date_source" in error for error in report.errors)


def test_legacy_gap_range_fields_are_rejected() -> None:
    page = load_json(project_root() / "evaluation" / "goldens" / "page-110.json")
    block = page["blocks"][0]
    block["date_start_source"] = block.pop("start_date")["date_source"]
    assert schema_errors(page, load_canonical_schema())


def test_owner_approved_page181_preserves_visible_1898_as_normalized() -> None:
    page = load_json(project_root() / "edition" / "owner-approved" / "page-181.json")
    entry = next(
        block
        for block in page["blocks"]
        if (block.get("date") or {}).get("date_source") == "Monday July 11 - 1898"
    )
    assert entry["date"]["date_normalized"] == "1898-07-11"
    assert entry["date"]["normalization_status"] == "normalized"
    assert entry["content"][0]["lines"][0]["segments"][0]["text"].endswith("1898")
    assert page["editorial_notes"] == []


def test_canonical_builder_and_source_validation(source_root: Path, tmp_path: Path) -> None:
    source = SourcePackage(source_root)
    draft = load_json(project_root() / "tests" / "fixtures" / "model" / "page-039.json")
    prompt = tmp_path / "prompt.md"
    write_text(prompt, "test prompt")
    page = build_candidate_page(
        draft=draft,
        source=source,
        page=39,
        run_id="test-run",
        backend="fixture",
        model="fixture",
        reasoning_effort="none",
        prompt_path=prompt,
        schema_path=project_root() / "schema" / "model-output.schema.json",
        context_pages=[38, 40],
        created_at="2026-08-08T18:00:00Z",
    )
    report = validate_page(page, source)
    assert report.valid, report.errors


def test_agent_review_cannot_claim_owner_authority(source_root: Path, tmp_path: Path) -> None:
    source = SourcePackage(source_root)
    draft = load_json(project_root() / "tests" / "fixtures" / "model" / "page-039.json")
    prompt = tmp_path / "prompt.md"
    write_text(prompt, "test prompt")
    page = build_candidate_page(
        draft=draft,
        source=source,
        page=39,
        run_id="test-run",
        backend="fixture",
        model="fixture",
        reasoning_effort="none",
        prompt_path=prompt,
        schema_path=project_root() / "schema" / "model-output.schema.json",
        context_pages=[38, 40],
        created_at="2026-08-08T18:00:00Z",
    )
    page["review"] = {
        "status": "agent_reviewed",
        "authority": "owner_approved_edition",
        "reviewer": "Codex",
        "reviewed_at": "2026-08-08T18:00:00Z",
        "notes": ["invalid authority escalation"],
    }
    report = validate_page(page, source)
    assert not report.valid
    assert any("diagnostic_only" in error for error in report.errors)


def test_source_date_status_coherence(source_root: Path, tmp_path: Path) -> None:
    source = SourcePackage(source_root)
    draft = load_json(project_root() / "tests" / "fixtures" / "model" / "page-181.json")
    prompt = tmp_path / "prompt.md"
    write_text(prompt, "test prompt")
    page = build_candidate_page(
        draft=draft,
        source=source,
        page=181,
        run_id="test-run",
        backend="fixture",
        model="fixture",
        reasoning_effort="none",
        prompt_path=prompt,
        schema_path=project_root() / "schema" / "model-output.schema.json",
        context_pages=[180, 182],
        created_at="2026-08-08T18:00:00Z",
    )
    broken = deepcopy(page)
    broken["blocks"][0]["date"]["normalization_reason"] = None
    report = validate_page(broken, source)
    assert not report.valid
    assert any("requires normalized date" in error for error in report.errors)


def test_block_order_date_and_provenance_invariants(
    source_root: Path, tmp_path: Path
) -> None:
    source = SourcePackage(source_root)
    draft = load_json(project_root() / "tests" / "fixtures" / "model" / "page-181.json")
    prompt = tmp_path / "prompt.md"
    write_text(prompt, "test prompt")
    page = build_candidate_page(
        draft=draft,
        source=source,
        page=181,
        run_id="test-run",
        backend="fixture",
        model="fixture",
        reasoning_effort="none",
        prompt_path=prompt,
        schema_path=project_root() / "schema" / "model-output.schema.json",
        context_pages=[180, 182],
        created_at="2026-08-08T18:00:00Z",
    )

    wrong_order = deepcopy(page)
    wrong_order["blocks"][0]["order"] = 2
    assert any("orders must be contiguous" in error for error in validate_page(wrong_order, source).errors)

    wrong_date = deepcopy(page)
    wrong_date["blocks"][0]["date"]["date_normalized"] = "1898-02-30"
    assert not validate_page(wrong_date, source).valid

    wrong_source = deepcopy(page)
    wrong_source["source"]["image"]["path"] = "pages/page-180.png"
    assert any("Image path must be pages/page-181.png" in error for error in validate_page(wrong_source, source).errors)


def test_serialization_and_agent_review_remain_diagnostic(
    source_root: Path, tmp_path: Path
) -> None:
    source = SourcePackage(source_root)
    draft = load_json(project_root() / "tests" / "fixtures" / "model" / "page-039.json")
    prompt = tmp_path / "prompt.md"
    write_text(prompt, "test prompt")
    page = build_candidate_page(
        draft=draft,
        source=source,
        page=39,
        run_id="test-run",
        backend="fixture",
        model="fixture",
        reasoning_effort="none",
        prompt_path=prompt,
        schema_path=project_root() / "schema" / "model-output.schema.json",
        context_pages=[38, 40],
        created_at="2026-08-08T18:00:00Z",
    )
    candidate = tmp_path / "candidate.json"
    write_json(candidate, page)
    assert load_json(candidate) == page
    staged = stage_agent_review(
        reviewed_path=candidate,
        source=source,
        reviewer="Codex",
        notes=["Image checked for diagnostic use."],
        golden_path=tmp_path / "golden.json",
        edition_path=tmp_path / "edition.json",
    )
    assert staged["review"]["status"] == "agent_reviewed"
    assert staged["review"]["authority"] == "diagnostic_only"
    assert "not independent ground truth" in staged["review"]["notes"][0]


def test_unreadable_deletion_preserves_deletion_structure(
    source_root: Path, tmp_path: Path
) -> None:
    source = SourcePackage(source_root)
    draft = load_json(project_root() / "tests" / "fixtures" / "model" / "page-181.json")
    segment = draft["blocks"][0]["content"][0]["lines"][0]["segments"][0]
    segment.clear()
    segment.update(
        {
            "type": "deletion",
            "medium": "typewritten",
            "method": "erased",
            "reading": None,
            "alternatives": [],
            "confidence": "unreadable",
        }
    )
    page = build_candidate_page(
        draft=draft,
        source=source,
        page=181,
        run_id="test-run",
        backend="fixture",
        model="fixture",
        reasoning_effort="none",
        prompt_path=project_root() / "prompts" / "transcribe-page.md",
        schema_path=project_root() / "schema" / "model-output.schema.json",
        context_pages=[],
        created_at="2026-08-08T18:00:00Z",
    )
    assert validate_page(page, source).valid


def test_graphical_feature_is_observational_and_non_textual(
    source_root: Path,
) -> None:
    source = SourcePackage(source_root)
    page = load_json(project_root() / "edition" / "owner-approved" / "page-039.json")
    page["schema_version"] = "1.2.0"
    page["page_role"] = "other"
    page["blocks"] = [
        {
            "id": "p039-b001",
            "order": 1,
            "type": "graphical_feature",
            "medium": "handwritten",
            "placement": "page",
            "description": "Small handwritten mark near the center of the page.",
            "region": {"x": 100, "y": 200, "width": 40, "height": 30},
            "related_block_ids": [],
            "continuation": {
                "from_previous": "no",
                "to_next": "no",
                "previous": None,
                "next": None,
            },
            "evidence": {
                "status": "not_attempted",
                "method": None,
                "ocr_word_ids": [],
                "bbox": None,
                "matched_tokens": 0,
                "total_tokens": 0,
            },
        }
    ]
    assert validate_page(page, source).valid

    continued = deepcopy(page)
    continued["blocks"][0]["continuation"]["to_next"] = "yes"
    assert not validate_page(continued, source).valid
