"""Tests for stage 040 element-quartet validation.

WHY:
    Hermetic: each test builds a minimal element quartet on disk in a tmp_path
    rather than depending on the accountable-record data repo, so the tests
    pin s040's structural contract independently of any real dataset. Mirrors
    the structural failure cases the stage is specified to catch.

OBS:
    A PipelineState with a non-None sources artifact is constructed so the
    stage's SOURCES_REQUIRED gate is satisfied and discovery actually runs.
    The minimal valid quartet mirrors the real observed shape (schema tags,
    [identity].compact_id, [classification].component_group matching the
    on-disk category, checks element_type_id matching compact_id).
"""

import json
from pathlib import Path

import pytest

from accountable_record.pipeline.context import (
    PipelineRunContext,
    PipelineState,
)
from accountable_record.stage_030_sources.model import SourceRegistry, SourcesArtifact
from accountable_record.stage_040_elements.base import constants
from accountable_record.stage_040_elements.transform import transform_elements
from accountable_record.stage_040_elements.validation import (
    validate_elements_stage,
)

# --- fixtures and builders ---------------------------------------------------

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

[identity]
compact_id = "se.accountable-record.{category}.{name}"
local_name = "{name}"
label = "{label}"

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

[structure]
schema = "schema.schema.json"
checks = "checks.toml"
examples = "examples/"
"""

VALID_CHECKS_TOML = """\
schema = "ar-element-checks-1"

element_type_id = "se.accountable-record.{category}.{name}"

[[checks]]
id = "{name}.has-element-type-id"
label = "Has element type ID"
level = "required"
"""

VALID_SCHEMA_JSON = {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "https://structural-explainability.org/x.schema.json",
    "title": "X",
    "type": "object",
    "required": ["element_type_id", "element_instance_id"],
    "properties": {
        "element_type_id": {"type": "string"},
        "element_instance_id": {"type": "string"},
    },
    "additionalProperties": True,
}

VALID_EXAMPLE_JSON = {
    "element_type_id": "se.accountable-record.identity.version",
    "element_instance_id": "version-001",
}


def write_quartet(
    elements_root: Path,
    category: str,
    name: str,
    *,
    omit: str | None = None,
    bad_toml: bool = False,
    bad_json: bool = False,
) -> Path:
    """Write a minimal element quartet under elements_root/category/name.

    Args:
        elements_root: The data/elements/ directory.
        category: Element category (becomes component_group and the path seg).
        name: Element name (the element directory).
        omit: Optional quartet member filename to NOT write (relative form,
            e.g. "checks.toml" or "examples/valid.json").
        bad_toml: When True, write element.toml as invalid TOML.
        bad_json: When True, write examples/valid.json as invalid JSON.

    Returns:
        The element directory path.
    """
    element_dir = elements_root / category / name
    (element_dir / constants.EXAMPLES_DIRNAME).mkdir(parents=True, exist_ok=True)

    fmt = {"category": category, "name": name, "label": name.title()}

    if omit != constants.ELEMENT_FILENAME:
        content = "not valid toml =" if bad_toml else VALID_ELEMENT_TOML.format(**fmt)
        (element_dir / constants.ELEMENT_FILENAME).write_text(content, encoding="utf-8")

    if omit != constants.CHECKS_FILENAME:
        (element_dir / constants.CHECKS_FILENAME).write_text(
            VALID_CHECKS_TOML.format(**fmt), encoding="utf-8"
        )

    if omit != constants.SCHEMA_FILENAME:
        (element_dir / constants.SCHEMA_FILENAME).write_text(
            json.dumps(VALID_SCHEMA_JSON), encoding="utf-8"
        )

    example_rel = f"{constants.EXAMPLES_DIRNAME}/{constants.EXAMPLE_VALID_FILENAME}"
    if omit != example_rel:
        example_content = (
            "{not valid json" if bad_json else json.dumps(VALID_EXAMPLE_JSON)
        )
        (
            element_dir / constants.EXAMPLES_DIRNAME / constants.EXAMPLE_VALID_FILENAME
        ).write_text(example_content, encoding="utf-8")

    return element_dir


def make_state(target_root: Path) -> PipelineState:
    """Build a PipelineState with sources satisfied so s040 runs.

    OBS:
        s040 now narrows state.sources to SourcesArtifact, so the test must
        provide the real stage 030 artifact type rather than an object sentinel.
    """
    context = PipelineRunContext.from_target(target_root, strict=True)
    state = PipelineState.initial(context)

    sources = SourcesArtifact(
        target_root=str(target_root),
        registry=SourceRegistry(),
    )

    return state.with_sources(sources, state.diagnostics)


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


def test_valid_quartets_discovered(tmp_path: Path) -> None:
    """Two valid quartets are discovered and produce an elements artifact."""
    elements_root = tmp_path / constants.ELEMENTS_DIRNAME
    write_quartet(elements_root, "identity", "version")
    write_quartet(elements_root, "sources", "source")

    result = validate_elements_stage(make_state(tmp_path))

    assert not result.has_errors
    assert result.elements is not None
    assert result.elements.registry.count == 2
    pairs = result.elements.registry.category_name_pairs()
    assert ("identity", "version") in pairs
    assert ("sources", "source") in pairs


def test_missing_elements_dir_fails(tmp_path: Path) -> None:
    """An absent data/elements/ fails structurally with the dir-missing code."""
    result = validate_elements_stage(make_state(tmp_path))

    assert result.has_errors
    assert result.elements is None
    codes = {d.code.name for d in result.diagnostics.items}
    assert "ELEMENTS_DIR_MISSING" in codes


def test_missing_quartet_member_fails(tmp_path: Path) -> None:
    """An element directory missing checks.toml fails structurally."""
    elements_root = tmp_path / constants.ELEMENTS_DIRNAME
    write_quartet(elements_root, "identity", "version", omit=constants.CHECKS_FILENAME)

    result = validate_elements_stage(make_state(tmp_path))

    assert result.has_errors
    assert result.elements is None


def test_invalid_toml_fails_with_path(tmp_path: Path) -> None:
    """Unparseable element.toml fails and names the offending file path."""
    elements_root = tmp_path / constants.ELEMENTS_DIRNAME
    write_quartet(elements_root, "identity", "version", bad_toml=True)

    result = validate_elements_stage(make_state(tmp_path))

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


def test_invalid_json_fails_with_path(tmp_path: Path) -> None:
    """Unparseable examples/valid.json fails and names the offending path."""
    elements_root = tmp_path / constants.ELEMENTS_DIRNAME
    write_quartet(elements_root, "identity", "version", bad_json=True)

    result = validate_elements_stage(make_state(tmp_path))

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


def test_state_updated_through_with_elements(tmp_path: Path) -> None:
    """A passing stage folds the artifact into state via with_elements."""
    elements_root = tmp_path / constants.ELEMENTS_DIRNAME
    write_quartet(elements_root, "identity", "version")

    state = make_state(tmp_path)
    new_state = transform_elements(state)

    assert not new_state.has_failed
    assert new_state.elements is not None


def test_category_component_group_mismatch_fails(tmp_path: Path) -> None:
    """A component_group not matching the on-disk category fails structurally."""
    elements_root = tmp_path / constants.ELEMENTS_DIRNAME
    element_dir = write_quartet(elements_root, "identity", "version")
    # Rewrite element.toml so component_group disagrees with the path category.
    bad = VALID_ELEMENT_TOML.format(
        category="identity", name="version", label="Version"
    ).replace('component_group = "identity"', 'component_group = "wrong"')
    (element_dir / constants.ELEMENT_FILENAME).write_text(bad, encoding="utf-8")

    result = validate_elements_stage(make_state(tmp_path))

    assert result.has_errors


@pytest.mark.parametrize("missing", ["element.toml", "schema.schema.json"])
def test_each_required_member_missing_fails(tmp_path: Path, missing: str) -> None:
    """Omitting any required quartet member fails structurally."""
    elements_root = tmp_path / constants.ELEMENTS_DIRNAME
    write_quartet(elements_root, "identity", "version", omit=missing)

    result = validate_elements_stage(make_state(tmp_path))

    assert result.has_errors
