# ============================================================
# tests/test_stage_060_exports.py
# ============================================================
"""Tests for stage 060 export validation and catalog reconciliation.

WHY:
    Hermetic: each test constructs a real s050 CatalogArtifact and writes the
    matching export pairs under tmp_path, rather than depending on the
    accountable-record data repo.

OBS:
    A PipelineState is built with the catalog artifact folded in via
    with_catalog() so the CATALOG_REQUIRED gate is satisfied. Repository
    profile controls whether target export material is expected. The
    element-scope rule is exercised by writing a non-element export and
    asserting it is not treated as an uncataloged orphan.
"""

import json
from pathlib import Path

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_050_catalog.model import (
    Catalog,
    CatalogArtifact,
    CatalogEntry,
    CatalogReconciliation,
)
from accountable_record.stage_060_exports.base import constants
from accountable_record.stage_060_exports.transform import transform_exports
from accountable_record.stage_060_exports.validation import validate_exports_stage

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


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 make_catalog_entry(category: str, name: str) -> CatalogEntry:
    """Build a minimal CatalogEntry for a category/name pair."""
    compact_id = compact_id_for(category, name)
    return CatalogEntry(
        compact_id=compact_id,
        component_group=category,
        local_name=name,
        label=name.title(),
        artifact_kind="element-type",
        canonical_uri=f"https://x/{category}/{name}",
        persistent_id=f"urn:ar:se:accountable-record:{category}:{name}",
        version="0.1.0",
        status="working-draft",
        source=f"data/elements/{category}/{name}/element.toml",
    )


def make_context(
    repo_root: Path,
    *,
    validates_target_materials: bool,
) -> PipelineRunContext:
    """Build a pipeline context with an explicit repository profile."""
    repo_class = "contract" if validates_target_materials else "implementation"
    layer_role = (
        "language-neutral-contract"
        if validates_target_materials
        else "verifier-implementation"
    )

    return PipelineRunContext(
        target_root=repo_root,
        strict=True,
        repository_profile=RepositoryProfile(
            repo_class=repo_class,
            layer_role=layer_role,
            validates_target_materials=validates_target_materials,
        ),
    )


def make_catalog_state(
    repo_root: Path,
    pairs: list[tuple[str, str]],
    *,
    validates_target_materials: bool,
) -> PipelineState:
    """Build a PipelineState with a CatalogArtifact for the given elements."""
    entries = tuple(make_catalog_entry(category, name) for category, name in pairs)
    artifact = CatalogArtifact(
        target_root=str(repo_root),
        catalog=Catalog(entries=entries),
        reconciliation=CatalogReconciliation(declared_present=True),
    )
    context = make_context(
        repo_root,
        validates_target_materials=validates_target_materials,
    )
    state = PipelineState.initial(context)
    return state.with_catalog(artifact, Diagnostics())


def write_element_export(repo_root: Path, category: str, name: str) -> None:
    """Write a valid element export pair under data/export/elements/."""
    export_dir = (
        repo_root
        / constants.EXPORT_DIRNAME
        / constants.EXPORT_ELEMENTS_SUBDIR
        / category
        / name
    )
    export_dir.mkdir(parents=True, exist_ok=True)

    compact_id = compact_id_for(category, name)
    element_json = {
        "schema": "ar-element-type-1",
        "identity": {
            "compact_id": compact_id,
            "local_name": name,
            "label": name.title(),
        },
        "classification": {
            "artifact_kind": "element-type",
            "component_group": category,
        },
    }
    checks_json = {
        "schema": "ar-element-checks-1",
        "element_type_id": compact_id,
        "checks": [],
    }

    (export_dir / constants.EXPORT_ELEMENT_FILENAME).write_text(
        json.dumps(element_json),
        encoding="utf-8",
    )
    (export_dir / constants.EXPORT_CHECKS_FILENAME).write_text(
        json.dumps(checks_json),
        encoding="utf-8",
    )


def write_non_element_export(repo_root: Path) -> None:
    """Write a non-element export under data/export/ outside elements/."""
    other = repo_root / constants.EXPORT_DIRNAME / "vocabulary"
    other.mkdir(parents=True, exist_ok=True)
    (other / "terms.json").write_text(json.dumps({"terms": []}), encoding="utf-8")


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


def test_implementation_repo_missing_export_root_passes_with_empty_registry(
    tmp_path: Path,
) -> None:
    """Implementation repos do not require target data/export/ material."""
    state = make_catalog_state(
        tmp_path,
        [("identity", "version")],
        validates_target_materials=False,
    )

    result = validate_exports_stage(state)

    assert not result.has_errors
    assert result.exports is not None
    assert result.exports.registry.count == 0
    assert result.exports.reconciliation.is_consistent


def test_missing_catalog_fails(tmp_path: Path) -> None:
    """state.catalog is None fails with CATALOG_REQUIRED."""
    context = make_context(tmp_path, validates_target_materials=False)
    state = PipelineState.initial(context)

    result = validate_exports_stage(state)

    assert result.has_errors
    codes = {diagnostic.code.name for diagnostic in result.diagnostics.items}
    assert "CATALOG_REQUIRED" in codes


def test_target_repo_missing_export_root_fails(tmp_path: Path) -> None:
    """Target-material repos require data/export/ when exports are expected."""
    state = make_catalog_state(
        tmp_path,
        [("identity", "version")],
        validates_target_materials=True,
    )

    result = validate_exports_stage(state)

    assert result.has_errors
    codes = {diagnostic.code.name for diagnostic in result.diagnostics.items}
    assert "EXPORT_ROOT_MISSING" in codes


def test_valid_element_export_discovered(tmp_path: Path) -> None:
    """Valid element exports parse and reconcile cleanly against the catalog."""
    pairs = [("identity", "version"), ("sources", "source")]
    state = make_catalog_state(
        tmp_path,
        pairs,
        validates_target_materials=True,
    )
    for category, name in pairs:
        write_element_export(tmp_path, category, name)

    result = validate_exports_stage(state)

    assert not result.has_errors
    assert result.exports is not None
    assert result.exports.registry.count == 2
    assert result.exports.reconciliation.is_consistent


def test_catalog_entry_without_export_fails(tmp_path: Path) -> None:
    """A catalog entry with no element export is reported."""
    state = make_catalog_state(
        tmp_path,
        [("identity", "version"), ("sources", "source")],
        validates_target_materials=True,
    )
    write_element_export(tmp_path, "identity", "version")

    result = validate_exports_stage(state)

    codes = {diagnostic.code.name for diagnostic in result.diagnostics.items}
    assert "CATALOG_ENTRY_EXPORT_MISSING" in codes
    assert result.has_errors


def test_export_without_catalog_entry_fails(tmp_path: Path) -> None:
    """An element export with no catalog entry is reported."""
    state = make_catalog_state(
        tmp_path,
        [("identity", "version")],
        validates_target_materials=True,
    )
    write_element_export(tmp_path, "identity", "version")
    write_element_export(tmp_path, "ghost", "phantom")

    result = validate_exports_stage(state)

    codes = {diagnostic.code.name for diagnostic in result.diagnostics.items}
    assert "EXPORT_ELEMENT_UNCATALOGED" in codes
    assert result.has_errors


def test_non_element_export_not_treated_as_orphan(tmp_path: Path) -> None:
    """A non-element export under data/export/ is not an uncataloged orphan."""
    state = make_catalog_state(
        tmp_path,
        [("identity", "version")],
        validates_target_materials=True,
    )
    write_element_export(tmp_path, "identity", "version")
    write_non_element_export(tmp_path)

    result = validate_exports_stage(state)

    assert not result.has_errors
    assert result.exports is not None
    assert result.exports.reconciliation.is_consistent


def test_state_updated_through_with_exports(tmp_path: Path) -> None:
    """A passing stage folds the exports artifact into state via with_exports."""
    state = make_catalog_state(
        tmp_path,
        [("identity", "version")],
        validates_target_materials=False,
    )

    new_state = transform_exports(state)

    assert not new_state.has_failed
    assert new_state.exports is not None
