"""Tests for stage 050 catalog build and reconciliation.

WHY:
    Hermetic: each test constructs a real s040 ElementsArtifact and writes the
    matching element.toml files (and, where relevant, a declared catalog) under
    tmp_path, rather than depending on the accountable-record data repo. This
    pins s050's build + reconciliation contract independently of any dataset.

OBS:
    A PipelineState is built with the elements artifact already folded in via
    with_elements(), so the ELEMENTS_REQUIRED gate is satisfied and the catalog
    actually builds.
"""

import json
from pathlib import Path

from accountable_record.pipeline.context import (
    PipelineRunContext,
    PipelineState,
)
from accountable_record.stage_040_elements.model import (
    ElementRecord,
    ElementRegistry,
    ElementsArtifact,
)
from accountable_record.stage_050_catalog.base import constants
from accountable_record.stage_050_catalog.transform import transform_catalog
from accountable_record.stage_050_catalog.validation import validate_catalog_stage

# --- builders ----------------------------------------------------------------

ELEMENT_TOML = """\
schema = "ar-element-type-1"

[identity]
canonical_uri = "https://structural-explainability.org/accountable-record/elements/{category}/{name}"
persistent_id = "urn:ar:se:accountable-record:{category}:{name}"
compact_id = "{compact_id}"
local_name = "{name}"
label = "{label}"

[classification]
artifact_kind = "element-type"
component_group = "{category}"

[release]
version = "0.1.0"
status = "working-draft"
"""


def compact_id_for(category: str, name: str) -> str:
    """Return the compact_id for a category/name pair."""
    return f"se.accountable-record.{category}.{name}"


def write_element_toml(repo_root: Path, category: str, name: str) -> ElementRecord:
    """Write a minimal valid element.toml and return the matching ElementRecord."""
    element_dir = repo_root / "data" / "elements" / category / name
    element_dir.mkdir(parents=True, exist_ok=True)

    compact_id = compact_id_for(category, name)
    (element_dir / constants.ELEMENT_FILENAME).write_text(
        ELEMENT_TOML.format(
            category=category,
            name=name,
            label=name.title(),
            compact_id=compact_id,
        ),
        encoding="utf-8",
    )

    return ElementRecord(
        category=category,
        name=name,
        rel_dir=f"data/elements/{category}/{name}",
        compact_id=compact_id,
    )


def make_elements_state(
    repo_root: Path,
    pairs: list[tuple[str, str]],
) -> PipelineState:
    """Build a PipelineState with an ElementsArtifact for the given elements."""
    records = tuple(write_element_toml(repo_root, c, n) for c, n in pairs)
    artifact = ElementsArtifact(
        target_root=str(repo_root),
        registry=ElementRegistry(records=records),
    )
    context = PipelineRunContext.from_target(repo_root, strict=True)
    state = PipelineState.initial(context)
    return state.with_elements(artifact, state.diagnostics)


def write_declared_catalog(repo_root: Path, compact_ids: list[str]) -> None:
    """Write a declared data/catalog/elements.json with the given compact_ids."""
    catalog_dir = repo_root / "data" / "catalog"
    catalog_dir.mkdir(parents=True, exist_ok=True)
    payload = {
        "count": len(compact_ids),
        "elements": [{"compact_id": cid} for cid in compact_ids],
        "schema": constants.CATALOG_SCHEMA_TAG,
    }
    (catalog_dir / "elements.json").write_text(json.dumps(payload), encoding="utf-8")


# --- tests -------------------------------------------------------------------


def test_catalog_built_from_elements(tmp_path: Path) -> None:
    """The catalog is built over the validated elements, sorted by compact_id."""
    state = make_elements_state(
        tmp_path, [("identity", "version"), ("sources", "source")]
    )

    result = validate_catalog_stage(state)

    assert result.catalog is not None
    catalog = result.catalog.catalog
    assert catalog.count == 2
    ids = [entry.compact_id for entry in catalog.entries]
    assert ids == sorted(ids)
    assert compact_id_for("identity", "version") in catalog.compact_ids()


def test_catalog_entry_enriched_fields(tmp_path: Path) -> None:
    """A built entry carries descriptive fields read from element.toml."""
    state = make_elements_state(tmp_path, [("identity", "version")])

    result = validate_catalog_stage(state)

    assert result.catalog is not None
    entry = result.catalog.catalog.entries[0]
    assert entry.component_group == "identity"
    assert entry.local_name == "version"
    assert entry.version == "0.1.0"
    assert entry.status == "working-draft"


def test_reconciliation_clean_when_declared_matches(tmp_path: Path) -> None:
    """No diffs when the declared catalog matches the built catalog."""
    pairs = [("identity", "version"), ("sources", "source")]
    state = make_elements_state(tmp_path, pairs)
    write_declared_catalog(tmp_path, [compact_id_for(c, n) for c, n in pairs])

    result = validate_catalog_stage(state)

    assert result.catalog is not None
    recon = result.catalog.reconciliation
    assert recon.declared_present
    assert recon.is_consistent
    assert not result.has_errors


def test_reconciliation_reports_undeclared(tmp_path: Path) -> None:
    """An element absent from the declared catalog is reported undeclared."""
    state = make_elements_state(
        tmp_path, [("identity", "version"), ("sources", "source")]
    )
    # Declared catalog omits sources/source.
    write_declared_catalog(tmp_path, [compact_id_for("identity", "version")])

    result = validate_catalog_stage(state)

    codes = {d.code.name for d in result.diagnostics.items}
    assert "CATALOG_ENTRY_UNDECLARED" in codes
    assert result.has_errors


def test_reconciliation_reports_orphaned(tmp_path: Path) -> None:
    """A declared entry with no validated element is reported orphaned."""
    state = make_elements_state(tmp_path, [("identity", "version")])
    write_declared_catalog(
        tmp_path,
        [compact_id_for("identity", "version"), compact_id_for("ghost", "missing")],
    )

    result = validate_catalog_stage(state)

    codes = {d.code.name for d in result.diagnostics.items}
    assert "CATALOG_ENTRY_ORPHANED" in codes
    assert result.has_errors


def test_missing_declared_catalog_warns_not_fails(tmp_path: Path) -> None:
    """Absent declared catalog is a warning (skip), not a stage failure."""
    state = make_elements_state(tmp_path, [("identity", "version")])
    # No declared catalog written.

    result = validate_catalog_stage(state)

    assert result.catalog is not None
    assert not result.has_errors
    codes = {d.code.name for d in result.diagnostics.items}
    assert "DECLARED_CATALOG_MISSING" in codes


def test_malformed_declared_catalog_fails(tmp_path: Path) -> None:
    """A present-but-unparseable declared catalog fails with the path named."""
    state = make_elements_state(tmp_path, [("identity", "version")])
    catalog_dir = tmp_path / "data" / "catalog"
    catalog_dir.mkdir(parents=True, exist_ok=True)
    (catalog_dir / "elements.json").write_text("{not json", encoding="utf-8")

    result = validate_catalog_stage(state)

    assert result.has_errors
    locations = [d.location for d in result.diagnostics.items if d.location]
    assert any(loc and "elements.json" in loc for loc in locations)


def test_elements_gate_blocks_without_elements(tmp_path: Path) -> None:
    """Without an elements artifact, s050 reports ELEMENTS_REQUIRED and stops."""
    context = PipelineRunContext.from_target(tmp_path, strict=True)
    state = PipelineState.initial(context)  # no elements

    result = validate_catalog_stage(state)

    assert result.has_errors
    codes = {d.code.name for d in result.diagnostics.items}
    assert "ELEMENTS_REQUIRED" in codes


def test_state_updated_through_with_catalog(tmp_path: Path) -> None:
    """A passing stage folds the catalog artifact into state via with_catalog."""
    pairs = [("identity", "version")]
    state = make_elements_state(tmp_path, pairs)
    write_declared_catalog(tmp_path, [compact_id_for(c, n) for c, n in pairs])

    new_state = transform_catalog(state)

    assert not new_state.has_failed
    assert new_state.catalog is not None


def test_serializable_shape_matches_declared(tmp_path: Path) -> None:
    """The in-memory serializable form has {count, elements, schema}."""
    state = make_elements_state(tmp_path, [("identity", "version")])

    result = validate_catalog_stage(state)

    assert result.catalog is not None
    serializable = result.catalog.catalog.serializable()
    assert set(serializable.keys()) == {"count", "elements", "schema"}
    assert serializable["schema"] == constants.CATALOG_SCHEMA_TAG
    assert serializable["count"] == 1
