# ============================================================
# tests/test_stage_070_resolution.py
# ============================================================
"""Tests for stage 070 resolution validation."""

import json
from pathlib import Path

import pytest

from accountable_record.pipeline.context import PipelineRunContext, PipelineState
from accountable_record.pipeline.diagnostics import Diagnostics
from accountable_record.pipeline.repository_profile import RepositoryProfile
from accountable_record.stage_060_exports.model import (
    ExportElementRecord,
    ExportReconciliation,
    ExportRegistry,
    ExportsArtifact,
)
from accountable_record.stage_070_resolution.base import constants
from accountable_record.stage_070_resolution.base import diagnostics as dx
from accountable_record.stage_070_resolution.graph import build_resolution_graph
from accountable_record.stage_070_resolution.lock import load_elements_lock
from accountable_record.stage_070_resolution.model import (
    ElementLockRecord,
    ElementsLock,
    PackageLockRecord,
    ResolutionPolicy,
)
from accountable_record.stage_070_resolution.validation import validate_resolution_stage
from accountable_record.stage_070_resolution.verify import reconcile_lock_with_exports

VALID_DIGEST = "a" * 64


def make_context(
    repo_root: Path,
    *,
    validates_target_materials: bool = False,
) -> PipelineRunContext:
    """Create a pipeline context for tests."""
    return PipelineRunContext(
        target_root=repo_root,
        repository_profile=RepositoryProfile(
            repo_class="implementation"
            if not validates_target_materials
            else "contract",
            layer_role="verifier-implementation"
            if not validates_target_materials
            else "language-neutral-contract",
            validates_target_materials=validates_target_materials,
        ),
    )


def write_manifest(repo_root: Path, *, locks: str | None = None) -> None:
    """Write a minimal manifest with optional resolution lock directive."""
    resolution = ""
    if locks is not None:
        resolution = f"""

[resolution]
locks = {locks!r}
"""

    (repo_root / "SE_MANIFEST.toml").write_text(
        f"""schema = "se-manifest-schema"
schema_url = "local"

[repo]
name = "fixture"
org = "structural-explainability"
class = "implementation"
kind = "implementation"
status = "active"
since = "2026"
summary = "Fixture."

[layer]
space = "application"
role = "verifier-implementation"

[contract]
name = "accountable-record"
org = "structural-explainability"
consumes_contract_from = "structural-explainability/accountable-record@main"

[depends]
required = []
optional = []

[provides]
artifacts = []

[scope]
includes = []
excludes = []

[validation]
entrypoint = "none"

[citation]
cff = "CITATION.cff"
preferred = "software"

[traceability]
identifier_map = "none"
{resolution}
""",
        encoding="utf-8",
    )


def write_valid_lock(repo_root: Path) -> Path:
    """Write a minimal valid data/locks/elements.lock.json."""
    lock_dir = repo_root / "data" / "locks"
    lock_dir.mkdir(parents=True, exist_ok=True)

    package_source = repo_root / "data" / "packages" / "core" / "package.toml"
    package_source.parent.mkdir(parents=True, exist_ok=True)
    package_source.write_text('schema = "ar-package-1"\n', encoding="utf-8")

    lock_path = lock_dir / "elements.lock.json"
    lock_path.write_text(
        json.dumps(
            {
                "schema": constants.ELEMENT_LOCK_SCHEMA_TAG,
                "resolved_at": "2026-05-22T17:12:49Z",
                "resolution_policy": {
                    "one_major_version_per_identity_per_graph": True,
                    "range_conflicts_are_errors": True,
                },
                "packages": [
                    {
                        "compact_id": "se.accountable-record.packages.core",
                        "version": "0.1.0",
                        "canonical_uri": (
                            "https://structural-explainability.org/"
                            "accountable-record/packages/core"
                        ),
                        "digest_algorithm": "sha256",
                        "digest_target": "canonical-generated-json",
                        "digest": VALID_DIGEST,
                        "source": "data/packages/core/package.toml",
                    }
                ],
                "elements": [
                    {
                        "compact_id": "se.accountable-record.identity.identifier",
                        "version": "0.1.0",
                        "included_by": ["se.accountable-record.packages.core@0.1.0"],
                        "digest_algorithm": "sha256",
                        "digest_target": "canonical-generated-json",
                        "digest": VALID_DIGEST,
                    }
                ],
            },
            indent=2,
        ),
        encoding="utf-8",
    )
    return lock_path


def make_state(
    repo_root: Path,
    *,
    exports: ExportsArtifact | None = None,
    validates_target_materials: bool = False,
) -> PipelineState:
    """Create a pipeline state for stage 070 tests."""
    context = make_context(
        repo_root,
        validates_target_materials=validates_target_materials,
    )
    state = PipelineState.initial(context)

    if exports is None:
        return state

    return state.with_exports(exports, Diagnostics())


def make_exports(*, compact_ids: tuple[str, ...]) -> ExportsArtifact:
    """Create an exports artifact with element export records."""
    records = tuple(
        ExportElementRecord(
            category="identity",
            name=compact_id.rsplit(".", maxsplit=1)[-1],
            rel_dir=f"data/export/elements/identity/{compact_id}",
            compact_id=compact_id,
        )
        for compact_id in compact_ids
    )
    return ExportsArtifact(
        target_root="fixture",
        registry=ExportRegistry(records=records),
        reconciliation=ExportReconciliation(),
    )


def test_validate_resolution_requires_exports(tmp_path: Path) -> None:
    """s070 requires the s060 exports artifact."""
    write_manifest(tmp_path)
    state = make_state(tmp_path)

    result = validate_resolution_stage(state)

    assert result.has_errors
    assert result.resolution is None
    assert result.diagnostics.items[0].code == dx.EXPORTS_REQUIRED.code


def test_validate_resolution_missing_lock_root_fails(tmp_path: Path) -> None:
    """When locks are expected, missing data/locks/ is structural failure."""
    write_manifest(tmp_path)
    state = make_state(tmp_path).with_exports(
        make_exports(compact_ids=()),
        Diagnostics(),
    )

    result = validate_resolution_stage(state)

    assert result.has_errors
    assert result.resolution is None
    assert result.diagnostics.items[0].code == dx.LOCK_ROOT_MISSING.code


def test_implementation_repo_with_locks_present_validates_locks(
    tmp_path: Path,
) -> None:
    """If implementation repo has data/locks/, s070 validates it rather than ignoring it."""
    write_manifest(tmp_path)
    write_valid_lock(tmp_path)
    state = make_state(
        tmp_path,
        exports=make_exports(
            compact_ids=("se.accountable-record.identity.identifier",)
        ),
        validates_target_materials=False,
    )

    result = validate_resolution_stage(state)

    assert not result.has_errors
    assert result.resolution is not None
    assert result.resolution.lock is not None
    assert result.resolution.graph is not None


def test_load_elements_lock_parses_valid_lock(tmp_path: Path) -> None:
    """A valid elements lock parses into the stage 070 model."""
    write_valid_lock(tmp_path)

    lock = load_elements_lock(tmp_path)

    assert lock.schema == constants.ELEMENT_LOCK_SCHEMA_TAG
    assert lock.policy.one_major_version_per_identity_per_graph is True
    assert lock.policy.range_conflicts_are_errors is True
    assert len(lock.packages) == 1
    assert len(lock.elements) == 1
    assert lock.packages[0].versioned_id == "se.accountable-record.packages.core@0.1.0"
    assert lock.elements[0].compact_id == "se.accountable-record.identity.identifier"


def test_build_resolution_graph_derives_bidirectional_edges() -> None:
    """The graph maps packages to elements and elements to packages."""
    lock = ElementsLock(
        target_root="fixture",
        rel_path=constants.ELEMENTS_LOCK_PATH,
        schema=constants.ELEMENT_LOCK_SCHEMA_TAG,
        resolved_at="2026-05-22T17:12:49Z",
        policy=ResolutionPolicy(
            one_major_version_per_identity_per_graph=True,
            range_conflicts_are_errors=True,
        ),
        packages=(
            PackageLockRecord(
                compact_id="se.accountable-record.packages.core",
                version="0.1.0",
                canonical_uri="https://example.test/core",
                digest_algorithm="sha256",
                digest_target="canonical-generated-json",
                digest=VALID_DIGEST,
                source="data/packages/core/package.toml",
            ),
        ),
        elements=(
            ElementLockRecord(
                compact_id="se.accountable-record.identity.identifier",
                version="0.1.0",
                included_by=("se.accountable-record.packages.core@0.1.0",),
                digest_algorithm="sha256",
                digest_target="canonical-generated-json",
                digest=VALID_DIGEST,
            ),
        ),
    )

    graph = build_resolution_graph(lock)

    assert graph.is_consistent
    assert graph.package_to_elements == {
        "se.accountable-record.packages.core@0.1.0": (
            "se.accountable-record.identity.identifier",
        )
    }
    assert graph.element_to_packages == {
        "se.accountable-record.identity.identifier": (
            "se.accountable-record.packages.core@0.1.0",
        )
    }


def test_reconcile_lock_with_exports_detects_parity() -> None:
    """Matching lock/export element ids reconcile cleanly."""
    lock = ElementsLock(
        target_root="fixture",
        rel_path=constants.ELEMENTS_LOCK_PATH,
        schema=constants.ELEMENT_LOCK_SCHEMA_TAG,
        resolved_at="2026-05-22T17:12:49Z",
        policy=ResolutionPolicy(
            one_major_version_per_identity_per_graph=True,
            range_conflicts_are_errors=True,
        ),
        packages=(),
        elements=(
            ElementLockRecord(
                compact_id="se.accountable-record.identity.identifier",
                version="0.1.0",
                included_by=(),
                digest_algorithm="sha256",
                digest_target="canonical-generated-json",
                digest=VALID_DIGEST,
            ),
        ),
    )
    exports = make_exports(compact_ids=("se.accountable-record.identity.identifier",))

    reconciliation = reconcile_lock_with_exports(lock, exports)

    assert reconciliation.is_consistent
    assert reconciliation.lock_missing_export == ()


def test_reconcile_lock_with_exports_detects_mismatches() -> None:
    """Lock/export reconciliation reports both directions."""
    lock = ElementsLock(
        target_root="fixture",
        rel_path=constants.ELEMENTS_LOCK_PATH,
        schema=constants.ELEMENT_LOCK_SCHEMA_TAG,
        resolved_at="2026-05-22T17:12:49Z",
        policy=ResolutionPolicy(
            one_major_version_per_identity_per_graph=True,
            range_conflicts_are_errors=True,
        ),
        packages=(),
        elements=(
            ElementLockRecord(
                compact_id="se.accountable-record.identity.identifier",
                version="0.1.0",
                included_by=(),
                digest_algorithm="sha256",
                digest_target="canonical-generated-json",
                digest=VALID_DIGEST,
            ),
        ),
    )
    exports = make_exports(
        compact_ids=("se.accountable-record.subjects.record-subject",)
    )

    reconciliation = reconcile_lock_with_exports(lock, exports)

    assert not reconciliation.is_consistent
    assert reconciliation.lock_missing_export == (
        "se.accountable-record.identity.identifier",
    )


@pytest.mark.parametrize(
    ("bad_digest", "expected_message"),
    [
        ("a" * 63, "64 hex characters"),
        ("g" * 64, "hexadecimal"),
    ],
)
def test_load_elements_lock_rejects_bad_digest(
    tmp_path: Path,
    bad_digest: str,
    expected_message: str,
) -> None:
    """Malformed digest declarations are rejected structurally."""
    lock_path = write_valid_lock(tmp_path)
    data = json.loads(lock_path.read_text(encoding="utf-8"))
    data["elements"][0]["digest"] = bad_digest
    lock_path.write_text(json.dumps(data), encoding="utf-8")

    with pytest.raises(Exception, match=expected_message):
        load_elements_lock(tmp_path)
