from __future__ import annotations

import hashlib
import json
import os
from pathlib import Path

import pytest
from reliable_memory import canonical_json_bytes  # noqa: E402


def _sha(value: bytes) -> str:
    return hashlib.sha256(value).hexdigest()


def _reference(daily_id: str, source: bytes, block: str, start: int, end: int) -> str:
    return (
        f"daily:{daily_id} sha256:{_sha(source)} block:{block} "
        f"bytes:{start}-{end}"
    )


def _write_bag(root: Path, daily_id: str, source: bytes, *, suffix: str = "one") -> Path:
    bag = root / "knowledge" / "daily" / "archive" / daily_id[:7] / f"bag-test-{suffix}"
    payload_name = f"data/{daily_id}.md"
    (bag / "data").mkdir(parents=True)
    (bag / payload_name).write_bytes(source)
    (bag / "bagit.txt").write_bytes(
        b"BagIt-Version: 1.0\nTag-File-Character-Encoding: UTF-8\n"
    )
    (bag / "bag-info.txt").write_bytes(
        (
            f"Bagging-Date: 2026-07-14\n"
            f"Payload-Oxum: {len(source)}.1\n"
            f"External-Identifier: daily:{daily_id}\n"
        ).encode()
    )
    (bag / "manifest-sha256.txt").write_bytes(
        f"{_sha(source)}  {payload_name}\n".encode()
    )
    block_start = source.index(b"## [")
    evidence = {
        "block_id": "evt-1",
        "byte_start": block_start,
        "byte_end": len(source),
        "line_start": 2,
        "line_end": 4,
        "sha256": _sha(source[block_start:]),
    }
    import compile_memory

    logical_path = f"knowledge/daily/{daily_id}.md"
    source_identity = compile_memory.compile_source_identity(
        logical_path, _sha(source)
    )
    receipt_path = root / f"knowledge/daily/receipts/v3-{source_identity}.md"
    receipt_hash = _sha(receipt_path.read_bytes()) if receipt_path.exists() else "a" * 64
    operation_id = "compile:test"
    if receipt_path.exists():
        operation_id = json.loads(
            receipt_path.read_text(encoding="utf-8")
            .split("```json\n", 1)[1]
            .split("\n```", 1)[0]
        )["operation_id"]
    manifest = {
        "schema_version": "archive-manifest/v1",
        "logical_daily_id": daily_id,
        "original_path": f"knowledge/daily/{daily_id}.md",
        "source_hash": _sha(source),
        "payload_hash": _sha(source),
        "compile_receipt_ref": {
            "schema": "compile-receipt-ref/v1",
            "path": f"knowledge/daily/receipts/v3-{source_identity}.md",
            "logical_path": logical_path,
            "source_digest": _sha(source),
            "source_identity": source_identity,
            "receipt_file_hash": receipt_hash,
        },
        "queue_preflight": {
            "checked_at": "2026-07-14T00:00:00Z",
            "passed": True,
            "blocking_task_ids": [],
        },
        "operations": [{"operation_id": operation_id, "state": "succeeded"}],
        "evidence": [evidence],
        "pins": [],
        "retention_days": 90,
    }
    (bag / "archive-manifest.json").write_bytes(canonical_json_bytes(manifest))
    tags = ("archive-manifest.json", "bag-info.txt", "bagit.txt", "manifest-sha256.txt")
    (bag / "tagmanifest-sha256.txt").write_bytes(
        "".join(f"{_sha((bag / name).read_bytes())}  {name}\n" for name in tags).encode()
    )
    return bag


def _authorize_archive_source(
    root: Path, source: bytes, monkeypatch: pytest.MonkeyPatch
) -> Path:
    import compile_memory
    from markdown_transaction import MarkdownCoordinator

    state_root = root / "state"
    state_root.mkdir()
    for relative in ("knowledge/daily/receipts", "knowledge/notes"):
        (root / relative).mkdir(parents=True, exist_ok=True)
    (root / "knowledge/index.md").write_bytes(b"# index\n")
    (root / "knowledge/log.md").write_bytes(b"# log\n")
    (root / "AGENTS.md").write_bytes(b"contract\n")
    daily = root / "knowledge/daily/2026-01-01.md"
    daily.write_bytes(source)
    monkeypatch.setattr(compile_memory, "ROOT", root)
    monkeypatch.setattr(compile_memory, "STATE_ROOT", state_root)
    monkeypatch.setattr(compile_memory, "MEMORY", root / "knowledge")
    monkeypatch.setattr(compile_memory, "DAILY_DIR", root / "knowledge/daily")
    monkeypatch.setattr(compile_memory, "KNOWLEDGE", root / "knowledge/notes")
    monkeypatch.setattr(compile_memory, "INDEX", root / "knowledge/index.md")
    monkeypatch.setattr(compile_memory, "LOG", root / "knowledge/log.md")
    monkeypatch.setattr(compile_memory, "AGENTS", root / "AGENTS.md")
    inputs = compile_memory.snapshot_compile_inputs([daily])
    batch = compile_memory.pack_compile_batches(inputs, model=None)[0]
    compile_memory.apply_compile_plan(
        inputs,
        {"schema_version": "compile-plan/v2", "operations": []},
        action_key="c" * 64,
        trigger="manual",
        coordinator=MarkdownCoordinator(root, state_root),
        completed_at="2026-07-14T00:00:00Z",
        batch=batch,
        provider_budget={
            "provider": "fake",
            "model": "fake-v1",
            "max_output_tokens": 4000,
        },
    )
    daily.unlink()
    return state_root


@pytest.fixture
def vault(tmp_path: Path) -> Path:
    (tmp_path / "knowledge" / "daily" / "archive").mkdir(parents=True)
    return tmp_path


def test_parse_requires_exact_canonical_logical_reference() -> None:
    from evidence_resolver import EvidenceRef

    value = f"daily:2026-01-01 sha256:{'a' * 64} block:evt-1 bytes:10-20"
    assert str(EvidenceRef.parse(value)) == value
    for invalid in (
        value + " trailing",
        value.replace("2026-01-01", "../secret"),
        value.replace("bytes:10-20", "bytes:20-10"),
        value.replace("block:evt-1", "block:../evt"),
        value.replace("sha256:", "sha256:A"),
    ):
        with pytest.raises(ValueError):
            EvidenceRef.parse(invalid)


def test_direct_construction_enforces_the_same_reference_contract() -> None:
    from evidence_resolver import EvidenceRef

    with pytest.raises(ValueError, match="daily ID"):
        EvidenceRef("../2026-01-01", "a" * 64, "evt-1", 1, 2)
    with pytest.raises(ValueError, match="SHA-256"):
        EvidenceRef("2026-01-01", "A" * 64, "evt-1", 1, 2)
    with pytest.raises(ValueError, match="block ID"):
        EvidenceRef("2026-01-01", "a" * 64, "../evt", 1, 2)
    with pytest.raises(ValueError, match="half-open"):
        EvidenceRef("2026-01-01", "a" * 64, "evt-1", 2, 2)


@pytest.mark.parametrize(
    "candidate",
    [
        "daily:2026-01-01",
        f"daily:2026-01-01 sha256:{'a' * 64} block:evt-1 bytes:1-2 trailing",
        f"xdaily:2026-01-01 sha256:{'a' * 64} block:evt-1 bytes:1-2",
        f"daily:2026-01-01 sha256:{'a' * 64} block:../evt bytes:1-2",
    ],
)
def test_extraction_rejects_every_malformed_daily_candidate(candidate: str) -> None:
    from evidence_resolver import extract_evidence_references

    with pytest.raises(ValueError, match="evidence reference"):
        extract_evidence_references(f"## Evidence\n- `{candidate}`\n")


def test_block_parser_uses_reference_block_id_grammar() -> None:
    from evidence_resolver import EvidenceResolutionError, _blocks

    with pytest.raises(EvidenceResolutionError, match="block ID"):
        _blocks(b"## [../evt] event\ntext\n")


def test_flat_resolution_uses_utf8_half_open_bytes_lines_and_block_hash(vault: Path) -> None:
    from evidence_resolver import EvidenceRef, EvidenceResolver

    source = "# day\n## [evt-1] event\nαβ line\nnext\n".encode()
    path = vault / "knowledge" / "daily" / "2026-01-01.md"
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_bytes(source)
    start = source.index("α".encode())
    end = start + len("αβ line".encode())

    result = EvidenceResolver(vault).resolve(
        EvidenceRef.parse(_reference("2026-01-01", source, "evt-1", start, end))
    )

    block = source[source.index(b"## [evt-1]") :]
    assert result.bytes == "αβ line".encode()
    assert result.sha256 == _sha(result.bytes)
    assert result.source_sha256 == _sha(source)
    assert result.block_sha256 == _sha(block)
    assert (result.byte_start, result.byte_end) == (start, end)
    assert (result.line_start, result.line_end) == (3, 4)
    assert result.location == "flat"


def test_resolution_rejects_non_utf8_boundary_outside_or_ambiguous_block(vault: Path) -> None:
    from evidence_resolver import EvidenceRef, EvidenceResolutionError, EvidenceResolver

    source = "## [evt-1] first\nα\n## [evt-1] second\nα\n".encode()
    path = vault / "knowledge" / "daily" / "2026-01-01.md"
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_bytes(source)
    alpha = source.index("α".encode())
    resolver = EvidenceResolver(vault)

    with pytest.raises(EvidenceResolutionError, match="ambiguous"):
        resolver.resolve(EvidenceRef.parse(_reference("2026-01-01", source, "evt-1", alpha, alpha + 2)))

    unique = b"preamble\n## [evt-1] event\n" + "α".encode() + b"\n"
    path.write_bytes(unique)
    alpha = unique.index("α".encode())
    with pytest.raises(EvidenceResolutionError, match="UTF-8"):
        resolver.resolve(EvidenceRef.parse(_reference("2026-01-01", unique, "evt-1", alpha + 1, alpha + 2)))
    with pytest.raises(EvidenceResolutionError, match="block"):
        resolver.resolve(EvidenceRef.parse(_reference("2026-01-01", unique, "evt-1", 0, 2)))


def test_flat_hash_mismatch_fails_closed_without_archive_fallback(vault: Path) -> None:
    from evidence_resolver import EvidenceRef, EvidenceResolutionError, EvidenceResolver

    archived = b"# day\n## [evt-1] event\narchived\n"
    _write_bag(vault, "2026-01-01", archived)
    flat = vault / "knowledge" / "daily" / "2026-01-01.md"
    flat.write_bytes(b"# day\n## [evt-1] event\nchanged\n")
    start = archived.index(b"archived")

    with pytest.raises(EvidenceResolutionError, match="hash mismatch"):
        EvidenceResolver(vault).resolve(
            EvidenceRef.parse(_reference("2026-01-01", archived, "evt-1", start, start + 8))
        )


def test_validated_archive_resolves_identically_and_ambiguity_fails(
    vault: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    from evidence_resolver import EvidenceRef, EvidenceResolutionError, EvidenceResolver

    source = b"# day\n## [evt-1] event\narchived bytes\n"
    state_root = _authorize_archive_source(vault, source, monkeypatch)
    first = _write_bag(vault, "2026-01-01", source)
    from archive_daily import DailyArchiver

    DailyArchiver._seal(first)
    start = source.index(b"archived bytes")
    ref = EvidenceRef.parse(_reference("2026-01-01", source, "evt-1", start, len(source) - 1))
    result = EvidenceResolver(vault, state_root=state_root).resolve(ref)
    assert result.bytes == b"archived bytes"
    assert result.location == "archive"

    second = _write_bag(vault, "2026-01-01", source, suffix="two")
    DailyArchiver._seal(second)
    with pytest.raises(EvidenceResolutionError, match="ambiguous"):
        EvidenceResolver(vault, state_root=state_root).resolve(ref)


def test_archive_validation_rejects_tamper_oversize_and_links(vault: Path) -> None:
    from evidence_resolver import EvidenceRef, EvidenceResolutionError, EvidenceResolver

    source = b"# day\n## [evt-1] event\narchived bytes\n"
    bag = _write_bag(vault, "2026-01-01", source)
    start = source.index(b"archived bytes")
    ref = EvidenceRef.parse(_reference("2026-01-01", source, "evt-1", start, len(source) - 1))
    (bag / "archive-manifest.json").write_bytes(b"{" + b" " * 1_100_000 + b"}")
    with pytest.raises(EvidenceResolutionError, match="exceeds"):
        EvidenceResolver(vault).resolve(ref)

    if hasattr(os, "symlink"):
        (bag / "archive-manifest.json").unlink()
        outside = vault / "outside.md"
        outside.write_bytes(source)
        try:
            os.symlink(outside, bag / "archive-manifest.json")
        except OSError:
            pytest.skip("links require privileges on this platform")
        with pytest.raises(EvidenceResolutionError, match="non-symlink|regular"):
            EvidenceResolver(vault).resolve(ref)


def test_archive_directory_limit_is_enforced_while_iterating(
    vault: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    import evidence_resolver

    month = vault / "knowledge/daily/archive/2026-01"
    month.mkdir()
    for name in ("one", "two", "three"):
        (month / name).mkdir()
    monkeypatch.setattr(evidence_resolver, "MAX_DIRECTORY_ENTRIES", 2)
    ref = evidence_resolver.EvidenceRef("2026-01-01", "a" * 64, "evt-1", 1, 2)

    with pytest.raises(evidence_resolver.EvidenceResolutionError, match="entry scan limit"):
        evidence_resolver.EvidenceResolver(vault)._resolve_archive(ref)
