"""Strictness tests for ABI contract schemas.

Verifies:
- Every .schema.json is valid Draft 2020-12 JSON Schema
- additionalProperties: false rejects unknown fields
- Required fields cause failures when omitted
- Enum fields reject out-of-range values
"""

from __future__ import annotations

import json

import jsonschema.validators
import pytest

from abi_control_core.contracts import CONTRACTS_DIR
from abi_control_core.sdk.validate import SchemaValidationError, validate_json

# ── Discover all schema files ───────────────────────────────────────────────

ALL_SCHEMA_FILES = sorted(p.name for p in CONTRACTS_DIR.glob("*.schema.json"))


def _load_schema(filename: str) -> dict:
    """Load a schema by filename from the contracts directory."""
    with open(CONTRACTS_DIR / filename, encoding="utf-8") as f:
        return json.load(f)


# ── 1. TestSchemaSelfValidity ───────────────────────────────────────────────


class TestSchemaSelfValidity:
    """Every .schema.json must be a valid JSON Schema Draft 2020-12 document."""

    @pytest.mark.parametrize(
        "schema_file",
        ALL_SCHEMA_FILES,
        ids=[f.replace(".schema.json", "") for f in ALL_SCHEMA_FILES],
    )
    def test_schema_is_valid_draft_2020_12(self, schema_file: str) -> None:
        schema = _load_schema(schema_file)
        # check_schema raises SchemaError if the schema itself is invalid
        jsonschema.validators.Draft202012Validator.check_schema(schema)


# ── 2. TestAdditionalPropertiesEnforcement ──────────────────────────────────

# Minimal valid fixtures for schemas that enforce additionalProperties: false.
# Each fixture must pass validation on its own before injecting the extra field.

_ADDITIONAL_PROPS_FIXTURES: list[tuple[str, dict]] = [
    (
        "tier2_event_envelope.schema.json",
        {
            "schema_version": "tier2-event-envelope/1.0",
            "event_id": "evt-001",
            "event_type": "RUN_STARTED",
            "timestamp": "2025-01-01T00:00:00Z",
            "run_id": "e2e00001",
        },
    ),
    (
        "tier2_span_envelope.schema.json",
        {
            "schema_version": "tier2-span-envelope/1.0",
            "span_id": "span-001",
            "trace_id": "trace-001",
            "name": "orchestration_run",
            "start_time": "2025-01-01T00:00:00Z",
            "end_time": "2025-01-01T00:00:12Z",
            "status": "ok",
        },
    ),
    (
        "tier2_policy_decision.schema.json",
        {
            "schema_version": "tier2-policy-decision/1.0",
            "run_id": "e2e00001",
            "policy_id": "default",
            "decision": "allow",
            "decided_at": "2025-01-01T00:00:00Z",
        },
    ),
    (
        "tier2_runtime_context.schema.json",
        {
            "schema_version": "tier2-runtime-context/1.0",
            "python_version": "3.12.0",
            "platform": "Linux",
            "architecture": "x86_64",
            "repo_version": "0.1.0",
            "workspace_path": "<WORKSPACE>",
        },
    ),
    (
        "tier2_summary.schema.json",
        {
            "schema_version": "tier2-summary/1.0",
            "total_events": 10,
            "events_by_type": {"RUN_STARTED": 1},
            "events_by_category": {"system": 1},
        },
    ),
    (
        "tier2_pack_manifest.schema.json",
        {
            "schema_version": "tier2-pack-manifest/1.0",
            "run_id": "e2e00001",
            "created_at": "2025-01-01T00:00:00Z",
            "components": {"plan_graph": "plan_graph.json"},
        },
    ),
]


class TestAdditionalPropertiesEnforcement:
    """Schemas with additionalProperties: false must reject unexpected fields."""

    @pytest.mark.parametrize(
        "schema_file,valid_fixture",
        _ADDITIONAL_PROPS_FIXTURES,
        ids=[f[0].replace(".schema.json", "") for f in _ADDITIONAL_PROPS_FIXTURES],
    )
    def test_extra_field_rejected(self, schema_file: str, valid_fixture: dict) -> None:
        # Sanity: the fixture itself must be valid
        validate_json(valid_fixture, schema_file)

        # Inject an unexpected property
        injected = {**valid_fixture, "__injected_extra": True}
        with pytest.raises(SchemaValidationError):
            validate_json(injected, schema_file)


# ── 3. TestRequiredFieldEnforcement ─────────────────────────────────────────

# Each tuple: (schema_file, field_to_remove, minimal_valid_fixture)

_REQUIRED_FIELD_CASES: list[tuple[str, str, dict]] = [
    (
        "run_manifest.schema.json",
        "run_id",
        {
            "schema_version": "run-manifest/1.0",
            "run_id": "abcd1234",
            "gate": "intent-gate",
            "mode": "exploration",
            "trigger": "manual",
            "started_at": "2026-02-25T10:00:00Z",
            "completed_at": "2026-02-25T10:05:00Z",
            "intent_gate_hash": "sha256:" + "a" * 64,
            "verdict": "pass",
            "summary": {
                "total": 5,
                "passed": 5,
                "failed": 0,
                "skipped": 0,
                "blocker": 0,
            },
        },
    ),
    (
        "event_envelope.schema.json",
        "event_id",
        {
            "schema_version": "event-envelope/1.0",
            "event_id": "evt-001",
            "event_type": "policy_decision",
            "ts": "2026-02-25T10:00:00Z",
            "run_id": "abcd1234",
        },
    ),
    (
        "policy_decision.schema.json",
        "decision",
        {
            "schema_version": "policy-decision/1.0",
            "run_id": "abcd1234",
            "policy_id": "INTENT-001",
            "decision": "allow",
            "decided_at": "2026-02-25T10:00:00Z",
        },
    ),
    (
        "span.schema.json",
        "span_id",
        {
            "span_id": "aabb0011",
            "name": "schema_validate",
            "start_time": "2026-02-25T10:00:00Z",
            "end_time": "2026-02-25T10:00:01Z",
            "status": "ok",
        },
    ),
]


class TestRequiredFieldEnforcement:
    """Removing a required field from a valid fixture must cause validation failure."""

    @pytest.mark.parametrize(
        "schema_file,field,valid_fixture",
        _REQUIRED_FIELD_CASES,
        ids=[f"{s.replace('.schema.json', '')}_missing_{f}" for s, f, _ in _REQUIRED_FIELD_CASES],
    )
    def test_missing_required_field_rejected(
        self, schema_file: str, field: str, valid_fixture: dict
    ) -> None:
        # Sanity: the complete fixture must validate
        validate_json(valid_fixture, schema_file)

        # Remove the required field
        broken = {k: v for k, v in valid_fixture.items() if k != field}
        with pytest.raises(SchemaValidationError) as exc_info:
            validate_json(broken, schema_file)
        # The error message should reference the missing field
        assert any(field in msg for _, msg in exc_info.value.errors)


# ── 4. TestEnumStrictness ───────────────────────────────────────────────────


class TestEnumStrictness:
    """Enum-constrained fields must reject values outside the allowed set."""

    def test_tier2_policy_decision_rejects_maybe(self) -> None:
        """tier2_policy_decision 'decision' only allows allow/deny/warn/escalate."""
        instance = {
            "schema_version": "tier2-policy-decision/1.0",
            "run_id": "e2e00001",
            "policy_id": "default",
            "decision": "maybe",
            "decided_at": "2025-01-01T00:00:00Z",
        }
        with pytest.raises(SchemaValidationError) as exc_info:
            validate_json(instance, "tier2_policy_decision.schema.json")
        assert any("decision" in ptr or "maybe" in msg for ptr, msg in exc_info.value.errors)

    def test_tier2_span_envelope_rejects_unknown_status(self) -> None:
        """tier2_span_envelope 'status' only allows ok/error."""
        instance = {
            "schema_version": "tier2-span-envelope/1.0",
            "span_id": "span-001",
            "trace_id": "trace-001",
            "name": "orchestration_run",
            "start_time": "2025-01-01T00:00:00Z",
            "end_time": "2025-01-01T00:00:12Z",
            "status": "unknown",
        }
        with pytest.raises(SchemaValidationError) as exc_info:
            validate_json(instance, "tier2_span_envelope.schema.json")
        assert any("status" in ptr or "unknown" in msg for ptr, msg in exc_info.value.errors)
