"""Schema integrity and contract rigor tests for spec_manifest.schema.json."""

import json
from pathlib import Path

import pytest
from jsonschema import ValidationError, validate
from jsonschema.validators import Draft202012Validator

SCHEMA_PATH = Path(__file__).resolve().parent.parent / "schemas" / "spec_manifest.schema.json"


def _load_schema() -> dict:
    """Load and parse the spec_manifest schema."""
    return json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))


def _valid_manifest() -> dict:
    """Return a minimal valid manifest for mutation-based negative tests."""
    return {
        "spec_version": "1.0",
        "spec_id": "spec-example",
        "prd_hash": "a" * 64,
        "requirements": [
            {
                "id": "REQ-001",
                "text": "The system shall do something.",
                "acceptance": ["Verify it does something."],
            }
        ],
        "requirements_index": [
            {
                "req_id": "REQ-001",
                "title": "Example Requirement",
                "acceptance_criteria": {
                    "given": ["system is operational"],
                    "when": ["requirement is evaluated"],
                    "then": ["it does something"],
                },
                "source_prd_section": "Example",
            }
        ],
    }


def _collect_refs(obj: object) -> list[str]:
    """Recursively collect all $ref values from a JSON-like structure."""
    refs: list[str] = []
    if isinstance(obj, dict):
        for key, value in obj.items():
            if key == "$ref":
                refs.append(value)
            else:
                refs.extend(_collect_refs(value))
    elif isinstance(obj, list):
        for item in obj:
            refs.extend(_collect_refs(item))
    return refs


class TestSchemaSelfValidity:
    """Verify that spec_manifest.schema.json is itself valid JSON Schema Draft 2020-12."""

    def test_schema_is_valid_draft_2020_12(self):
        schema = _load_schema()
        Draft202012Validator.check_schema(schema)

    def test_schema_declares_draft_2020_12(self):
        schema = _load_schema()
        assert schema.get("$schema") == "https://json-schema.org/draft/2020-12/schema"

    def test_schema_file_exists(self):
        assert SCHEMA_PATH.exists(), f"Schema file not found at {SCHEMA_PATH}"

    def test_schema_is_valid_json(self):
        text = SCHEMA_PATH.read_text(encoding="utf-8")
        parsed = json.loads(text)
        assert isinstance(parsed, dict)


class TestSchemaRefIntegrity:
    """Verify all $ref pointers resolve, or prove the schema is self-contained."""

    def test_no_unresolved_refs(self):
        schema = _load_schema()
        refs = _collect_refs(schema)
        if not refs:
            # Schema is self-contained with no $ref — assert that explicitly.
            assert refs == [], "Schema is self-contained: no $ref values present"
        else:
            # If $ref values exist, each must resolve within the schema.
            validator = Draft202012Validator(schema)
            for ref in refs:
                resolver = validator.resolver
                resolved = resolver.lookup(ref)
                assert resolved is not None, f"$ref '{ref}' did not resolve"

    def test_schema_is_self_contained(self):
        """Explicitly prove the schema contains zero $ref pointers."""
        schema = _load_schema()
        refs = _collect_refs(schema)
        assert len(refs) == 0, f"Expected self-contained schema but found $ref values: {refs}"


class TestNegativeValidation:
    """Validate that the schema rejects malformed manifests."""

    def test_missing_spec_version(self):
        doc = _valid_manifest()
        del doc["spec_version"]
        with pytest.raises(ValidationError, match="spec_version"):
            validate(doc, _load_schema())

    def test_missing_spec_id(self):
        doc = _valid_manifest()
        del doc["spec_id"]
        with pytest.raises(ValidationError, match="spec_id"):
            validate(doc, _load_schema())

    def test_missing_prd_hash(self):
        doc = _valid_manifest()
        del doc["prd_hash"]
        with pytest.raises(ValidationError, match="prd_hash"):
            validate(doc, _load_schema())

    def test_missing_requirements(self):
        doc = _valid_manifest()
        del doc["requirements"]
        with pytest.raises(ValidationError, match="requirements"):
            validate(doc, _load_schema())

    def test_empty_requirements_array(self):
        doc = _valid_manifest()
        doc["requirements"] = []
        with pytest.raises(ValidationError, match="minItems"):
            validate(doc, _load_schema())

    def test_bad_prd_hash_format(self):
        doc = _valid_manifest()
        doc["prd_hash"] = "not-a-valid-hash"
        with pytest.raises(ValidationError, match="pattern"):
            validate(doc, _load_schema())

    def test_bad_prd_hash_uppercase(self):
        doc = _valid_manifest()
        doc["prd_hash"] = "A" * 64
        with pytest.raises(ValidationError, match="pattern"):
            validate(doc, _load_schema())

    def test_bad_prd_hash_too_short(self):
        doc = _valid_manifest()
        doc["prd_hash"] = "a" * 63
        with pytest.raises(ValidationError, match="pattern"):
            validate(doc, _load_schema())

    def test_bad_spec_id_uppercase(self):
        doc = _valid_manifest()
        doc["spec_id"] = "Spec-Bad"
        with pytest.raises(ValidationError, match="pattern"):
            validate(doc, _load_schema())

    def test_bad_spec_id_starts_with_digit(self):
        doc = _valid_manifest()
        doc["spec_id"] = "1spec"
        with pytest.raises(ValidationError, match="pattern"):
            validate(doc, _load_schema())

    def test_wrong_spec_version(self):
        doc = _valid_manifest()
        doc["spec_version"] = "2.0"
        with pytest.raises(ValidationError):
            validate(doc, _load_schema())

    def test_extra_root_field(self):
        doc = _valid_manifest()
        doc["extra_field"] = "should not be here"
        with pytest.raises(ValidationError, match="additionalProperties"):
            validate(doc, _load_schema())

    def test_requirement_missing_id(self):
        doc = _valid_manifest()
        del doc["requirements"][0]["id"]
        with pytest.raises(ValidationError, match="id"):
            validate(doc, _load_schema())

    def test_requirement_missing_text(self):
        doc = _valid_manifest()
        del doc["requirements"][0]["text"]
        with pytest.raises(ValidationError, match="text"):
            validate(doc, _load_schema())

    def test_requirement_missing_acceptance(self):
        doc = _valid_manifest()
        del doc["requirements"][0]["acceptance"]
        with pytest.raises(ValidationError, match="acceptance"):
            validate(doc, _load_schema())

    def test_requirement_empty_acceptance_array(self):
        doc = _valid_manifest()
        doc["requirements"][0]["acceptance"] = []
        with pytest.raises(ValidationError, match="minItems"):
            validate(doc, _load_schema())

    def test_valid_manifest_passes(self):
        """Sanity check: the baseline valid manifest must pass validation."""
        validate(_valid_manifest(), _load_schema())


class TestSchemaCompleteness:
    """Verify the schema defines all fields used by compiler output."""

    def test_root_properties_include_required_fields(self):
        schema = _load_schema()
        root_props = set(schema["properties"].keys())
        expected = {"spec_version", "spec_id", "prd_hash", "requirements", "requirements_index"}
        assert expected <= root_props, f"Missing root properties: {expected - root_props}"

    def test_requirement_items_have_expected_properties(self):
        schema = _load_schema()
        req_items = schema["properties"]["requirements"]["items"]
        item_props = set(req_items["properties"].keys())
        expected = {"id", "text", "acceptance"}
        assert expected <= item_props, f"Missing requirement properties: {expected - item_props}"

    def test_all_root_required_fields_have_definitions(self):
        """Every field in 'required' must also appear in 'properties'."""
        schema = _load_schema()
        required = set(schema["required"])
        defined = set(schema["properties"].keys())
        missing = required - defined
        assert not missing, f"Required fields lack property definitions: {missing}"

    def test_all_requirement_required_fields_have_definitions(self):
        """Every required field in requirement items must have a property definition."""
        schema = _load_schema()
        req_items = schema["properties"]["requirements"]["items"]
        required = set(req_items["required"])
        defined = set(req_items["properties"].keys())
        missing = required - defined
        assert not missing, f"Required requirement fields lack definitions: {missing}"

    def test_spec_version_is_const(self):
        schema = _load_schema()
        assert schema["properties"]["spec_version"]["const"] == "1.0"

    def test_requirements_has_min_items(self):
        schema = _load_schema()
        assert schema["properties"]["requirements"]["minItems"] >= 1

    def test_root_disallows_additional_properties(self):
        schema = _load_schema()
        assert schema["additionalProperties"] is False

    def test_requirement_items_disallow_additional_properties(self):
        schema = _load_schema()
        req_items = schema["properties"]["requirements"]["items"]
        assert req_items["additionalProperties"] is False
