"""Tests for the spec loader utilities."""

from __future__ import annotations

from pathlib import Path

import pytest

from prisme.spec.fields import FieldSpec, FieldType
from prisme.spec.model import ModelSpec
from prisme.spec.stack import StackSpec
from prisme.utils.spec_loader import (
    SpecLoadError,
    SpecValidationError,
    _find_stack_spec,
    get_model_dependency_order,
    load_spec_from_dict,
    load_spec_from_file,
    validate_spec,
)


class TestLoadSpecFromFile:
    """Tests for load_spec_from_file."""

    def test_load_valid_spec(self, tmp_path: Path) -> None:
        spec_file = tmp_path / "models.py"
        spec_file.write_text(
            """\
from prisme.spec.stack import StackSpec
from prisme.spec.model import ModelSpec
from prisme.spec.fields import FieldSpec, FieldType

spec = StackSpec(
    name="test",
    version="1.0.0",
    models=[
        ModelSpec(
            name="Item",
            fields=[FieldSpec(name="name", type=FieldType.STRING, required=True)],
        )
    ],
)
"""
        )
        result = load_spec_from_file(spec_file)
        assert isinstance(result, StackSpec)
        assert result.name == "test"
        assert len(result.models) == 1

    def test_load_spec_via_stack_variable(self, tmp_path: Path) -> None:
        spec_file = tmp_path / "models.py"
        spec_file.write_text(
            """\
from prisme.spec.stack import StackSpec
from prisme.spec.model import ModelSpec
from prisme.spec.fields import FieldSpec, FieldType

stack = StackSpec(
    name="test",
    version="1.0.0",
    models=[
        ModelSpec(
            name="Item",
            fields=[FieldSpec(name="name", type=FieldType.STRING, required=True)],
        )
    ],
)
"""
        )
        result = load_spec_from_file(spec_file)
        assert result.name == "test"

    def test_load_spec_via_factory_function(self, tmp_path: Path) -> None:
        spec_file = tmp_path / "models.py"
        spec_file.write_text(
            """\
from prisme.spec.stack import StackSpec
from prisme.spec.model import ModelSpec
from prisme.spec.fields import FieldSpec, FieldType

def get_spec():
    return StackSpec(
        name="factory-test",
        version="1.0.0",
        models=[
            ModelSpec(
                name="Item",
                fields=[FieldSpec(name="name", type=FieldType.STRING, required=True)],
            )
        ],
    )
"""
        )
        result = load_spec_from_file(spec_file)
        assert result.name == "factory-test"

    def test_file_not_found(self, tmp_path: Path) -> None:
        with pytest.raises(SpecLoadError, match="not found"):
            load_spec_from_file(tmp_path / "nonexistent.py")

    def test_non_python_file(self, tmp_path: Path) -> None:
        spec_file = tmp_path / "models.yaml"
        spec_file.write_text("name: test")
        with pytest.raises(SpecLoadError, match="Python file"):
            load_spec_from_file(spec_file)

    def test_file_with_syntax_error(self, tmp_path: Path) -> None:
        spec_file = tmp_path / "bad.py"
        spec_file.write_text("def broken(\n")
        with pytest.raises(SpecLoadError, match="Error executing"):
            load_spec_from_file(spec_file)

    def test_file_without_spec(self, tmp_path: Path) -> None:
        spec_file = tmp_path / "empty.py"
        spec_file.write_text("x = 42\n")
        with pytest.raises(SpecLoadError, match="No StackSpec found"):
            load_spec_from_file(spec_file)

    def test_file_with_runtime_error(self, tmp_path: Path) -> None:
        spec_file = tmp_path / "runtime_err.py"
        spec_file.write_text("raise ValueError('boom')\n")
        with pytest.raises(SpecLoadError, match="Error executing"):
            load_spec_from_file(spec_file)

    def test_supports_relative_imports(self, tmp_path: Path) -> None:
        """Spec files can use relative imports from their directory."""
        specs_dir = tmp_path / "specs"
        specs_dir.mkdir()

        # Create a shared module
        (specs_dir / "shared.py").write_text(
            """\
from prisme.spec.model import ModelSpec
from prisme.spec.fields import FieldSpec, FieldType

item = ModelSpec(
    name="SharedItem",
    fields=[FieldSpec(name="name", type=FieldType.STRING, required=True)],
)
"""
        )

        # Create the spec file that imports from shared
        (specs_dir / "models.py").write_text(
            """\
from prisme.spec.stack import StackSpec
from shared import item

spec = StackSpec(
    name="import-test",
    version="1.0.0",
    models=[item],
)
"""
        )

        result = load_spec_from_file(specs_dir / "models.py")
        assert result.name == "import-test"
        assert result.models[0].name == "SharedItem"


class TestLoadSpecFromDict:
    """Tests for load_spec_from_dict."""

    def test_valid_dict(self) -> None:
        data = {
            "name": "test-project",
            "version": "1.0.0",
            "models": [
                {
                    "name": "Item",
                    "fields": [{"name": "title", "type": "string", "required": True}],
                }
            ],
        }
        result = load_spec_from_dict(data)
        assert isinstance(result, StackSpec)
        assert result.name == "test-project"

    def test_invalid_dict_raises_validation_error(self) -> None:
        with pytest.raises(SpecValidationError, match="Invalid specification"):
            load_spec_from_dict({"invalid": True})


class TestValidateSpec:
    """Tests for validate_spec."""

    def test_valid_spec_passes(self) -> None:
        spec = StackSpec(
            name="test",
            version="1.0.0",
            models=[
                ModelSpec(
                    name="Item",
                    fields=[FieldSpec(name="name", type=FieldType.STRING, required=True)],
                )
            ],
        )
        # Should not raise
        validate_spec(spec)

    def test_duplicate_model_names(self) -> None:
        spec = StackSpec(
            name="test",
            version="1.0.0",
            models=[
                ModelSpec(
                    name="Item",
                    fields=[FieldSpec(name="name", type=FieldType.STRING)],
                ),
                ModelSpec(
                    name="Item",
                    fields=[FieldSpec(name="title", type=FieldType.STRING)],
                ),
            ],
        )
        with pytest.raises(SpecValidationError, match="error"):
            validate_spec(spec)

    def test_duplicate_field_names(self) -> None:
        spec = StackSpec(
            name="test",
            version="1.0.0",
            models=[
                ModelSpec(
                    name="Item",
                    fields=[
                        FieldSpec(name="name", type=FieldType.STRING),
                        FieldSpec(name="name", type=FieldType.STRING),
                    ],
                ),
            ],
        )
        with pytest.raises(SpecValidationError, match="error"):
            validate_spec(spec)

    def test_invalid_foreign_key_reference(self) -> None:
        spec = StackSpec(
            name="test",
            version="1.0.0",
            models=[
                ModelSpec(
                    name="Order",
                    fields=[
                        FieldSpec(
                            name="customer_id",
                            type=FieldType.INTEGER,
                            references="NonExistent",
                        ),
                    ],
                ),
            ],
        )
        with pytest.raises(SpecValidationError, match="error"):
            validate_spec(spec)

    def test_invalid_relationship_target(self) -> None:
        from prisme.spec.model import RelationshipSpec

        spec = StackSpec(
            name="test",
            version="1.0.0",
            models=[
                ModelSpec(
                    name="Order",
                    fields=[FieldSpec(name="name", type=FieldType.STRING)],
                    relationships=[
                        RelationshipSpec(
                            name="items",
                            target_model="NonExistent",
                            type="one_to_many",
                        )
                    ],
                ),
            ],
        )
        with pytest.raises(SpecValidationError, match="error"):
            validate_spec(spec)


class TestGetModelDependencyOrder:
    """Tests for get_model_dependency_order."""

    def test_no_dependencies(self) -> None:
        spec = StackSpec(
            name="test",
            version="1.0.0",
            models=[
                ModelSpec(
                    name="A",
                    fields=[FieldSpec(name="name", type=FieldType.STRING)],
                ),
                ModelSpec(
                    name="B",
                    fields=[FieldSpec(name="name", type=FieldType.STRING)],
                ),
            ],
        )
        order = get_model_dependency_order(spec)
        assert set(order) == {"A", "B"}

    def test_linear_dependencies(self) -> None:
        spec = StackSpec(
            name="test",
            version="1.0.0",
            models=[
                ModelSpec(
                    name="Order",
                    fields=[
                        FieldSpec(
                            name="customer_id", type=FieldType.INTEGER, references="Customer"
                        ),
                    ],
                ),
                ModelSpec(
                    name="Customer",
                    fields=[FieldSpec(name="name", type=FieldType.STRING)],
                ),
            ],
        )
        order = get_model_dependency_order(spec)
        assert order.index("Customer") < order.index("Order")

    def test_self_reference_ignored(self) -> None:
        spec = StackSpec(
            name="test",
            version="1.0.0",
            models=[
                ModelSpec(
                    name="Node",
                    fields=[
                        FieldSpec(name="parent_id", type=FieldType.INTEGER, references="Node"),
                    ],
                ),
            ],
        )
        order = get_model_dependency_order(spec)
        assert order == ["Node"]

    def test_circular_dependency_handled(self) -> None:
        spec = StackSpec(
            name="test",
            version="1.0.0",
            models=[
                ModelSpec(
                    name="A",
                    fields=[FieldSpec(name="b_id", type=FieldType.INTEGER, references="B")],
                ),
                ModelSpec(
                    name="B",
                    fields=[FieldSpec(name="a_id", type=FieldType.INTEGER, references="A")],
                ),
            ],
        )
        order = get_model_dependency_order(spec)
        assert set(order) == {"A", "B"}


class TestFindStackSpec:
    """Tests for _find_stack_spec."""

    def test_finds_spec_variable(self) -> None:
        import types

        module = types.ModuleType("test")
        module.spec = StackSpec(  # type: ignore[attr-defined]
            name="test",
            version="1.0.0",
            models=[
                ModelSpec(
                    name="Item",
                    fields=[FieldSpec(name="name", type=FieldType.STRING)],
                )
            ],
        )
        result = _find_stack_spec(module, Path("test.py"))
        assert result.name == "test"

    def test_finds_stack_variable(self) -> None:
        import types

        module = types.ModuleType("test")
        module.stack = StackSpec(  # type: ignore[attr-defined]
            name="from-stack",
            version="1.0.0",
            models=[
                ModelSpec(
                    name="Item",
                    fields=[FieldSpec(name="name", type=FieldType.STRING)],
                )
            ],
        )
        result = _find_stack_spec(module, Path("test.py"))
        assert result.name == "from-stack"

    def test_finds_factory_function(self) -> None:
        import types

        module = types.ModuleType("test")

        def get_spec() -> StackSpec:
            return StackSpec(
                name="factory",
                version="1.0.0",
                models=[
                    ModelSpec(
                        name="Item",
                        fields=[FieldSpec(name="name", type=FieldType.STRING)],
                    )
                ],
            )

        module.get_spec = get_spec  # type: ignore[attr-defined]
        result = _find_stack_spec(module, Path("test.py"))
        assert result.name == "factory"

    def test_raises_if_not_found(self) -> None:
        import types

        module = types.ModuleType("test")
        module.something_else = 42  # type: ignore[attr-defined]
        with pytest.raises(SpecLoadError, match="No StackSpec found"):
            _find_stack_spec(module, Path("test.py"))

    def test_factory_error_raises(self) -> None:
        import types

        module = types.ModuleType("test")

        def get_spec() -> StackSpec:
            raise RuntimeError("factory broken")

        module.get_spec = get_spec  # type: ignore[attr-defined]
        with pytest.raises(SpecLoadError, match="Error calling"):
            _find_stack_spec(module, Path("test.py"))


class TestSpecLoadError:
    """Tests for SpecLoadError."""

    def test_has_path(self) -> None:
        err = SpecLoadError("test error", path=Path("test.py"))
        assert err.path == Path("test.py")
        assert "test error" in str(err)

    def test_no_path(self) -> None:
        err = SpecLoadError("test error")
        assert err.path is None


class TestSpecValidationError:
    """Tests for SpecValidationError."""

    def test_has_errors(self) -> None:
        errors = [{"type": "test", "msg": "test error"}]
        err = SpecValidationError("validation failed", errors=errors)
        assert len(err.errors) == 1

    def test_no_errors(self) -> None:
        err = SpecValidationError("validation failed")
        assert err.errors == []
