from __future__ import annotations

import pytest

import icsd.adapters.schema_validation as schema_validation
from icsd.adapters import JsonSchemaInvariant, PydanticModelInvariant
from icsd.core.errors import InvariantViolationError
from icsd.core.invariants import Invariant, InvariantSet
from icsd.core.transition import Transition


def _contract_schema() -> dict[str, object]:
    return {
        "type": "object",
        "required": ["status", "count"],
        "properties": {
            "status": {"type": "string", "enum": ["draft", "review", "approved"]},
            "count": {"type": "integer", "minimum": 0},
            "tags": {
                "type": "array",
                "items": {"type": "string", "minLength": 1},
                "maxItems": 3,
            },
        },
        "additionalProperties": False,
    }


def test_json_schema_invariant_accepts_valid_state(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(schema_validation, "_load_jsonschema_validator", lambda: None)
    invariants = InvariantSet(
        [
            Invariant(
                name="contract_schema",
                check=JsonSchemaInvariant(schema=_contract_schema()),
            )
        ]
    )

    candidate = invariants.construct_state(
        {"status": "review", "count": 2, "tags": ["phase-a", "phase-b"]}
    )

    assert candidate == {"status": "review", "count": 2, "tags": ["phase-a", "phase-b"]}


def test_json_schema_invariant_reports_structured_failures(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setattr(schema_validation, "_load_jsonschema_validator", lambda: None)
    invariants = InvariantSet(
        [
            Invariant(
                name="contract_schema",
                check=JsonSchemaInvariant(schema=_contract_schema()),
            )
        ]
    )

    failures = invariants.validate({"status": "invalid", "count": -1, "extra": "x"})

    validators = {failure.details.get("validator") for failure in failures}
    assert len(failures) >= 3
    assert validators.issuperset({"additionalProperties", "enum", "minimum"})
    assert all(failure.code == "json_schema_invalid" for failure in failures)
    assert all(str(failure.details.get("path", "")).startswith("$") for failure in failures)


def test_json_schema_invariant_blocks_invalid_transition_mutation() -> None:
    invariants = InvariantSet(
        [
            Invariant(
                name="contract_schema",
                check=JsonSchemaInvariant(schema=_contract_schema()),
            )
        ]
    )
    transition = Transition(
        name="archive_record",
        mutate=lambda state: state.update({"status": "archived"}),
        invariants=invariants,
    )

    with pytest.raises(InvariantViolationError):
        transition.apply(
            state={"status": "draft", "count": 1},
            entity_type="contract",
            entity_id="contract-001",
            actor="user:ops",
            authority="policy/contract-lifecycle",
        )


def test_pydantic_model_invariant_accepts_valid_state() -> None:
    pydantic = pytest.importorskip("pydantic")

    class ContractModel(pydantic.BaseModel):
        status: str = pydantic.Field(min_length=1)
        count: int = pydantic.Field(ge=0)

    invariants = InvariantSet(
        [
            Invariant(
                name="contract_model",
                check=PydanticModelInvariant(model=ContractModel),
            )
        ]
    )

    candidate = invariants.construct_state({"status": "draft", "count": 1})

    assert candidate == {"status": "draft", "count": 1}


def test_pydantic_model_invariant_reports_validation_paths() -> None:
    pydantic = pytest.importorskip("pydantic")

    class ContractModel(pydantic.BaseModel):
        status: str = pydantic.Field(min_length=1)
        count: int = pydantic.Field(ge=0)

    invariants = InvariantSet(
        [
            Invariant(
                name="contract_model",
                check=PydanticModelInvariant(model=ContractModel),
            )
        ]
    )

    failures = invariants.validate({"status": "", "count": -1})

    paths = {failure.details.get("path") for failure in failures}
    assert len(failures) == 2
    assert paths == {"$.status", "$.count"}
    assert all(failure.code == "pydantic_model_invalid" for failure in failures)


def test_pydantic_model_invariant_requires_optional_dependency(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setattr(schema_validation, "_load_pydantic_runtime", lambda: None)

    with pytest.raises(RuntimeError, match="requires the optional dependency 'pydantic'"):
        PydanticModelInvariant(model=type("ContractModel", (), {}))
