"""Tests for contract validation logic."""

from __future__ import annotations

import pytest

from pact.contracts import (
    extract_base_types,
    validate_all_contracts,
    validate_contract_completeness,
    validate_decomposition_coverage,
    validate_dependency_graph,
    validate_north_star,
    validate_test_suite,
    validate_type_references,
)
from pact.schemas import (
    ComponentContract,
    ContractTestSuite,
    DecompositionNode,
    DecompositionTree,
    ErrorCase,
    FieldSpec,
    FunctionContract,
    TestCase,
    TypeSpec,
)


def _make_contract(
    component_id: str = "test",
    name: str = "Test",
    types: list[TypeSpec] | None = None,
    functions: list[FunctionContract] | None = None,
    dependencies: list[str] | None = None,
) -> ComponentContract:
    return ComponentContract(
        component_id=component_id,
        name=name,
        description="Test component",
        types=types or [],
        functions=functions or [
            FunctionContract(
                name="do_thing",
                description="Does the thing",
                inputs=[FieldSpec(name="x", type_ref="str")],
                output_type="str",
            ),
        ],
        dependencies=dependencies or [],
    )


def _make_test_suite(
    component_id: str = "test",
    code: str = "def test_example(): pass",
) -> ContractTestSuite:
    return ContractTestSuite(
        component_id=component_id,
        contract_version=1,
        test_cases=[
            TestCase(id="t1", description="test", function="do_thing", category="happy_path"),
        ],
        generated_code=code,
    )


class TestValidateTypeReferences:
    def test_primitives_resolve(self):
        c = _make_contract(
            functions=[
                FunctionContract(
                    name="f", description="d",
                    inputs=[FieldSpec(name="x", type_ref="str")],
                    output_type="int",
                ),
            ],
        )
        errors = validate_type_references(c)
        assert errors == []

    def test_custom_type_resolves(self):
        c = _make_contract(
            types=[TypeSpec(name="Price", kind="struct", fields=[FieldSpec(name="amount", type_ref="float")])],
            functions=[
                FunctionContract(
                    name="calc", description="d",
                    inputs=[],
                    output_type="Price",
                ),
            ],
        )
        errors = validate_type_references(c)
        assert errors == []

    def test_unresolved_output_type(self):
        c = _make_contract(
            functions=[
                FunctionContract(
                    name="calc", description="d",
                    inputs=[],
                    output_type="UnknownType",
                ),
            ],
        )
        errors = validate_type_references(c)
        assert len(errors) == 1
        assert "UnknownType" in errors[0]

    def test_unresolved_input_type(self):
        c = _make_contract(
            functions=[
                FunctionContract(
                    name="calc", description="d",
                    inputs=[FieldSpec(name="x", type_ref="MissingType")],
                    output_type="str",
                ),
            ],
        )
        errors = validate_type_references(c)
        assert len(errors) == 1
        assert "MissingType" in errors[0]

    def test_unresolved_field_type(self):
        c = _make_contract(
            types=[TypeSpec(name="Foo", kind="struct", fields=[FieldSpec(name="bar", type_ref="Baz")])],
            functions=[
                FunctionContract(
                    name="f", description="d",
                    inputs=[], output_type="Foo",
                ),
            ],
        )
        errors = validate_type_references(c)
        assert any("Baz" in e for e in errors)

    def test_list_item_type(self):
        c = _make_contract(
            types=[TypeSpec(name="Items", kind="list", item_type="MissingItem")],
            functions=[
                FunctionContract(
                    name="f", description="d",
                    inputs=[], output_type="str",
                ),
            ],
        )
        errors = validate_type_references(c)
        assert any("MissingItem" in e for e in errors)


class TestValidateDependencyGraph:
    def test_acyclic(self):
        tree = DecompositionTree(
            root_id="root",
            nodes={
                "root": DecompositionNode(
                    component_id="root", name="R", description="r",
                    children=["a", "b"],
                ),
                "a": DecompositionNode(
                    component_id="a", name="A", description="a", parent_id="root",
                ),
                "b": DecompositionNode(
                    component_id="b", name="B", description="b", parent_id="root",
                ),
            },
        )
        errors = validate_dependency_graph(tree)
        assert errors == []

    def test_cycle_detected(self):
        tree = DecompositionTree(
            root_id="a",
            nodes={
                "a": DecompositionNode(
                    component_id="a", name="A", description="a",
                    children=["b"],
                ),
                "b": DecompositionNode(
                    component_id="b", name="B", description="b",
                    children=["a"],  # cycle!
                ),
            },
        )
        errors = validate_dependency_graph(tree)
        assert any("cycle" in e.lower() for e in errors)

    def test_missing_child(self):
        tree = DecompositionTree(
            root_id="root",
            nodes={
                "root": DecompositionNode(
                    component_id="root", name="R", description="r",
                    children=["missing"],
                ),
            },
        )
        errors = validate_dependency_graph(tree)
        assert any("missing" in e for e in errors)


class TestValidateContractCompleteness:
    def test_valid(self):
        c = _make_contract()
        errors = validate_contract_completeness(c)
        assert errors == []

    def test_no_functions(self):
        c = ComponentContract(
            component_id="test", name="Test", description="t",
            functions=[],
        )
        errors = validate_contract_completeness(c)
        assert any("no functions" in e for e in errors)

    def test_missing_component_id(self):
        c = ComponentContract(
            component_id="", name="Test", description="t",
        )
        errors = validate_contract_completeness(c)
        assert any("component_id" in e for e in errors)

    def test_missing_name(self):
        c = ComponentContract(
            component_id="test", name="", description="t",
        )
        errors = validate_contract_completeness(c)
        assert any("name" in e.lower() for e in errors)

    def test_function_missing_output(self):
        c = _make_contract(
            functions=[
                FunctionContract(
                    name="f", description="d",
                    inputs=[], output_type="",
                ),
            ],
        )
        errors = validate_contract_completeness(c)
        assert any("output_type" in e for e in errors)


class TestValidateTestSuite:
    def test_valid(self):
        s = _make_test_suite()
        errors = validate_test_suite(s)
        assert errors == []

    def test_no_cases(self):
        s = ContractTestSuite(component_id="test", contract_version=1)
        errors = validate_test_suite(s)
        assert any("no test cases" in e for e in errors)

    def test_syntax_error(self):
        s = _make_test_suite(code="def test_bad(:\n    pass")
        errors = validate_test_suite(s)
        assert any("syntax error" in e.lower() for e in errors)

    def test_missing_component_id(self):
        s = ContractTestSuite(
            component_id="", contract_version=1,
            test_cases=[TestCase(id="t", description="d", function="f", category="happy_path")],
        )
        errors = validate_test_suite(s)
        assert any("component_id" in e for e in errors)


class TestValidateAllContracts:
    def test_all_valid(self):
        tree = DecompositionTree(
            root_id="a",
            nodes={
                "a": DecompositionNode(component_id="a", name="A", description="a"),
            },
        )
        contracts = {"a": _make_contract(component_id="a")}
        suites = {"a": _make_test_suite(component_id="a")}

        gate = validate_all_contracts(tree, contracts, suites)
        assert gate.passed

    def test_missing_contract(self):
        tree = DecompositionTree(
            root_id="a",
            nodes={
                "a": DecompositionNode(component_id="a", name="A", description="a"),
            },
        )
        gate = validate_all_contracts(tree, {}, {})
        assert not gate.passed
        assert any("missing contract" in d for d in gate.details)

    def test_missing_test_suite(self):
        tree = DecompositionTree(
            root_id="a",
            nodes={
                "a": DecompositionNode(component_id="a", name="A", description="a"),
            },
        )
        contracts = {"a": _make_contract(component_id="a")}
        gate = validate_all_contracts(tree, contracts, {})
        assert not gate.passed
        assert any("missing test suite" in d for d in gate.details)

    def test_unresolved_dependency(self):
        """Internal dep (in tree) without contract -> error.
        External dep (not in tree) is allowed.
        """
        tree = DecompositionTree(
            root_id="a",
            nodes={
                "a": DecompositionNode(
                    component_id="a", name="A", description="a",
                    children=["missing_dep"],
                ),
                "missing_dep": DecompositionNode(
                    component_id="missing_dep", name="M", description="m",
                    parent_id="a",
                ),
            },
        )
        c = _make_contract(component_id="a", dependencies=["missing_dep"])
        contracts = {"a": c}
        suites = {"a": _make_test_suite(component_id="a")}

        gate = validate_all_contracts(tree, contracts, suites)
        assert not gate.passed
        assert any("missing_dep" in d for d in gate.details)

    def test_multiple_components(self):
        tree = DecompositionTree(
            root_id="root",
            nodes={
                "root": DecompositionNode(
                    component_id="root", name="Root", description="r",
                    children=["a", "b"],
                ),
                "a": DecompositionNode(
                    component_id="a", name="A", description="a", parent_id="root",
                ),
                "b": DecompositionNode(
                    component_id="b", name="B", description="b", parent_id="root",
                ),
            },
        )
        contracts = {
            "root": _make_contract(component_id="root", dependencies=["a", "b"]),
            "a": _make_contract(component_id="a"),
            "b": _make_contract(component_id="b"),
        }
        suites = {
            "root": _make_test_suite(component_id="root"),
            "a": _make_test_suite(component_id="a"),
            "b": _make_test_suite(component_id="b"),
        }
        gate = validate_all_contracts(tree, contracts, suites)
        assert gate.passed

    def test_dependency_name_normalization(self):
        """Case-mismatch dependency should be resolved via normalization."""
        tree = DecompositionTree(
            root_id="root",
            nodes={
                "root": DecompositionNode(
                    component_id="root", name="Root", description="r",
                    children=["my_component"],
                ),
                "my_component": DecompositionNode(
                    component_id="my_component", name="MC", description="m",
                    parent_id="root",
                ),
            },
        )
        # Root depends on "My_Component" (case mismatch with "my_component")
        contracts = {
            "root": _make_contract(component_id="root", dependencies=["My_Component"]),
            "my_component": _make_contract(component_id="my_component"),
        }
        suites = {
            "root": _make_test_suite(component_id="root"),
            "my_component": _make_test_suite(component_id="my_component"),
        }
        gate = validate_all_contracts(tree, contracts, suites)
        assert gate.passed  # Normalization should resolve the case mismatch


class TestExtractBaseTypes:
    def test_simple_type(self):
        assert extract_base_types("str") == ["str"]

    def test_list_parameterized(self):
        assert extract_base_types("list[str]") == ["list", "str"]

    def test_dict_parameterized(self):
        assert extract_base_types("dict[str, int]") == ["dict", "str", "int"]

    def test_optional(self):
        assert extract_base_types("Optional[Foo]") == ["Optional", "Foo"]

    def test_nested(self):
        result = extract_base_types("list[dict[str, Bar]]")
        assert "list" in result
        assert "dict" in result
        assert "str" in result
        assert "Bar" in result

    def test_pipe_union(self):
        result = extract_base_types("str | None")
        assert "str" in result
        assert "None" in result

    def test_empty_string(self):
        assert extract_base_types("") == []

    def test_whitespace(self):
        assert extract_base_types("  str  ") == ["str"]

    def test_complex_nested(self):
        result = extract_base_types("dict[str, list[Optional[Foo]]]")
        assert "dict" in result
        assert "str" in result
        assert "list" in result
        assert "Optional" in result
        assert "Foo" in result


class TestParameterizedTypeValidation:
    """Validate that parameterized types like list[str] pass validation."""

    def test_list_of_primitive(self):
        c = _make_contract(
            functions=[
                FunctionContract(
                    name="f", description="d",
                    inputs=[FieldSpec(name="x", type_ref="list[str]")],
                    output_type="str",
                ),
            ],
        )
        errors = validate_type_references(c)
        assert errors == []

    def test_dict_of_primitives(self):
        c = _make_contract(
            functions=[
                FunctionContract(
                    name="f", description="d",
                    inputs=[FieldSpec(name="x", type_ref="dict[str, int]")],
                    output_type="str",
                ),
            ],
        )
        errors = validate_type_references(c)
        assert errors == []

    def test_optional_custom_type(self):
        c = _make_contract(
            types=[TypeSpec(name="Foo", kind="struct", fields=[FieldSpec(name="x", type_ref="str")])],
            functions=[
                FunctionContract(
                    name="f", description="d",
                    inputs=[],
                    output_type="Optional[Foo]",
                ),
            ],
        )
        errors = validate_type_references(c)
        assert errors == []

    def test_list_of_unknown_type_fails(self):
        c = _make_contract(
            functions=[
                FunctionContract(
                    name="f", description="d",
                    inputs=[FieldSpec(name="x", type_ref="list[UnknownType]")],
                    output_type="str",
                ),
            ],
        )
        errors = validate_type_references(c)
        assert len(errors) == 1
        assert "UnknownType" in errors[0]

    def test_pipe_union_resolves(self):
        c = _make_contract(
            functions=[
                FunctionContract(
                    name="f", description="d",
                    inputs=[FieldSpec(name="x", type_ref="str | None")],
                    output_type="str",
                ),
            ],
        )
        errors = validate_type_references(c)
        assert errors == []

    def test_struct_field_parameterized(self):
        c = _make_contract(
            types=[TypeSpec(name="Foo", kind="struct", fields=[
                FieldSpec(name="items", type_ref="list[str]"),
            ])],
            functions=[
                FunctionContract(
                    name="f", description="d",
                    inputs=[], output_type="Foo",
                ),
            ],
        )
        errors = validate_type_references(c)
        assert errors == []


class TestValidateNorthStar:
    """Tests for north-star validation — do contracts fulfill the original task?"""

    def _make_tree(self, nodes_data: dict[str, dict]) -> DecompositionTree:
        nodes = {}
        root_id = ""
        for nid, data in nodes_data.items():
            if not root_id:
                root_id = nid
            nodes[nid] = DecompositionNode(
                component_id=nid,
                name=data.get("name", nid),
                description=data.get("description", ""),
                children=data.get("children", []),
                parent_id=data.get("parent_id", ""),
            )
        return DecompositionTree(root_id=root_id, nodes=nodes)

    def test_no_warnings_when_verbs_covered(self):
        tree = self._make_tree({"root": {"name": "Root", "description": "Root"}})
        contracts = {
            "root": _make_contract(
                "root",
                functions=[
                    FunctionContract(name="parse_data", description="Parse input", inputs=[], output_type="str"),
                    FunctionContract(name="validate_result", description="Validate output", inputs=[], output_type="bool"),
                ],
            ),
        }
        warnings = validate_north_star("parse and validate the data", tree, contracts)
        assert not warnings

    def test_warns_when_task_verbs_uncovered(self):
        tree = self._make_tree({"root": {"name": "Root", "description": "Root"}})
        contracts = {
            "root": _make_contract(
                "root",
                functions=[
                    FunctionContract(name="get_data", description="Get data", inputs=[], output_type="str"),
                ],
            ),
        }
        warnings = validate_north_star(
            "audit and monitor and report the system", tree, contracts,
        )
        assert any("audit" in w or "monitor" in w for w in warnings)

    def test_warns_on_empty_root_functions(self):
        tree = self._make_tree({"root": {"name": "Root", "description": "Root"}})
        contracts = {
            "root": ComponentContract(
                component_id="root", name="Root", description="Root",
                functions=[],
            ),
        }
        warnings = validate_north_star("build something", tree, contracts)
        assert any("no functions" in w.lower() for w in warnings)

    def test_warns_on_empty_leaf_functions(self):
        tree = self._make_tree({
            "root": {"name": "Root", "description": "Root", "children": ["leaf"]},
            "leaf": {"name": "Leaf", "description": "Leaf", "parent_id": "root"},
        })
        contracts = {
            "root": _make_contract("root"),
            "leaf": ComponentContract(
                component_id="leaf", name="Leaf", description="Leaf",
                functions=[],
            ),
        }
        warnings = validate_north_star("do something", tree, contracts)
        assert any("leaf" in w and "no functions" in w.lower() for w in warnings)

    def test_empty_task_no_warnings(self):
        tree = self._make_tree({"root": {"name": "R", "description": "R"}})
        contracts = {"root": _make_contract("root")}
        assert validate_north_star("", tree, contracts) == []

    def test_empty_contracts_no_warnings(self):
        tree = self._make_tree({"root": {"name": "R", "description": "R"}})
        assert validate_north_star("build something", tree, {}) == []


class TestValidateDecompositionCoverage:
    """Tests for early decomposition validation."""

    def _make_tree(self, nodes_data: dict[str, dict]) -> DecompositionTree:
        nodes = {}
        root_id = ""
        for nid, data in nodes_data.items():
            if not root_id:
                root_id = nid
            nodes[nid] = DecompositionNode(
                component_id=nid,
                name=data.get("name", nid),
                description=data.get("description", "A component"),
                children=data.get("children", []),
                parent_id=data.get("parent_id", ""),
            )
        return DecompositionTree(root_id=root_id, nodes=nodes)

    def test_valid_tree_no_warnings(self):
        tree = self._make_tree({
            "root": {"name": "Root", "description": "Root system for parsing", "children": ["a", "b"]},
            "a": {"name": "A", "description": "Component handles parsing operations", "parent_id": "root"},
            "b": {"name": "B", "description": "Component handles validation operations", "parent_id": "root"},
        })
        warnings = validate_decomposition_coverage("parsing and validation operations", tree)
        assert not warnings

    def test_warns_on_empty_description(self):
        tree = self._make_tree({
            "root": {"name": "Root", "description": "Root", "children": ["a"]},
            "a": {"name": "A", "description": "", "parent_id": "root"},
        })
        warnings = validate_decomposition_coverage("", tree)
        assert any("empty description" in w.lower() for w in warnings)

    def test_warns_on_orphan_node(self):
        tree = self._make_tree({
            "root": {"name": "Root", "description": "Root"},
            "orphan": {"name": "Orphan", "description": "Lost node"},
        })
        warnings = validate_decomposition_coverage("", tree)
        assert any("orphan" in w.lower() for w in warnings)

    def test_warns_on_duplicate_descriptions(self):
        tree = self._make_tree({
            "root": {"name": "Root", "description": "Root system", "children": ["a", "b"]},
            "a": {"name": "A", "description": "This component handles all the data processing and transformation", "parent_id": "root"},
            "b": {"name": "B", "description": "This component handles all the data processing and transformation", "parent_id": "root"},
        })
        warnings = validate_decomposition_coverage("", tree)
        assert any("identical descriptions" in w.lower() for w in warnings)

    def test_warns_on_low_keyword_coverage(self):
        tree = self._make_tree({
            "root": {"name": "Root", "description": "Root manages logging", "children": ["a"]},
            "a": {"name": "A", "description": "A does logging too", "parent_id": "root"},
        })
        warnings = validate_decomposition_coverage(
            "authenticate users encrypt passwords validate tokens authorize requests", tree,
        )
        assert any("keyword" in w.lower() for w in warnings)

    def test_empty_tree_warns(self):
        tree = DecompositionTree(root_id="r", nodes={})
        warnings = validate_decomposition_coverage("", tree)
        assert warnings

    def test_none_tree_warns(self):
        warnings = validate_decomposition_coverage("", None)
        assert warnings
