"""Tests for the generator base classes."""

from __future__ import annotations

from pathlib import Path

import pytest

from prisme.generators.base import (
    CompositeGenerator,
    GeneratedFile,
    GeneratorBase,
    GeneratorContext,
    GeneratorResult,
    ModelGenerator,
    create_init_file,
)
from prisme.spec.fields import FieldSpec, FieldType
from prisme.spec.model import ModelSpec
from prisme.spec.project import ProjectSpec
from prisme.spec.stack import FileStrategy, StackSpec


@pytest.fixture
def basic_stack() -> StackSpec:
    return StackSpec(
        name="test-project",
        version="1.0.0",
        models=[
            ModelSpec(
                name="Item",
                fields=[FieldSpec(name="name", type=FieldType.STRING, required=True)],
            )
        ],
    )


@pytest.fixture
def project_spec() -> ProjectSpec:
    return ProjectSpec(name="test-project")


@pytest.fixture
def context(basic_stack: StackSpec, project_spec: ProjectSpec, tmp_path: Path) -> GeneratorContext:
    return GeneratorContext(
        domain_spec=basic_stack,
        output_dir=tmp_path,
        project_spec=project_spec,
    )


@pytest.fixture
def dry_run_context(
    basic_stack: StackSpec, project_spec: ProjectSpec, tmp_path: Path
) -> GeneratorContext:
    return GeneratorContext(
        domain_spec=basic_stack,
        output_dir=tmp_path,
        project_spec=project_spec,
        dry_run=True,
    )


class TestGeneratedFile:
    """Tests for GeneratedFile dataclass."""

    def test_defaults(self) -> None:
        gf = GeneratedFile(path=Path("test.py"), content="# test")
        assert gf.strategy == FileStrategy.ALWAYS_OVERWRITE
        assert gf.description == ""
        assert gf.has_hooks is False
        assert gf.extends is None

    def test_path_string_conversion(self) -> None:
        gf = GeneratedFile(path="test.py", content="# test")  # type: ignore[arg-type]
        assert isinstance(gf.path, Path)

    def test_extends_string_conversion(self) -> None:
        gf = GeneratedFile(path=Path("test.py"), content="# test", extends="base.py")  # type: ignore[arg-type]
        assert isinstance(gf.extends, Path)


class TestGeneratorContext:
    """Tests for GeneratorContext."""

    def test_spec_alias(self, context: GeneratorContext) -> None:
        assert context.spec is context.domain_spec

    def test_defaults(self, basic_stack: StackSpec, tmp_path: Path) -> None:
        ctx = GeneratorContext(domain_spec=basic_stack, output_dir=tmp_path)
        assert ctx.dry_run is False
        assert ctx.force is False
        assert ctx.protected_marker == "PRISM:PROTECTED"
        assert ctx.backend_module_name is None
        assert ctx.project_spec is None
        assert ctx.config is None


class TestGeneratorResult:
    """Tests for GeneratorResult."""

    def test_empty_is_success(self) -> None:
        result = GeneratorResult()
        assert result.success is True

    def test_with_errors_is_not_success(self) -> None:
        result = GeneratorResult(errors=["something failed"])
        assert result.success is False

    def test_defaults(self) -> None:
        result = GeneratorResult()
        assert result.files == []
        assert result.written == 0
        assert result.skipped == 0
        assert result.errors == []
        assert result.warnings == []


class _SimpleGenerator(GeneratorBase):
    """Test generator that produces a single file."""

    def generate_files(self) -> list[GeneratedFile]:
        return [
            GeneratedFile(
                path=Path("output.py"),
                content="# generated",
                strategy=FileStrategy.ALWAYS_OVERWRITE,
            )
        ]


class _FailingGenerator(GeneratorBase):
    """Test generator that raises during generation."""

    def generate_files(self) -> list[GeneratedFile]:
        raise ValueError("generation failed")


class TestGeneratorBase:
    """Tests for GeneratorBase."""

    def test_initialization(self, context: GeneratorContext) -> None:
        gen = _SimpleGenerator(context)
        assert gen.context is context
        assert gen.spec is context.domain_spec
        assert gen.output_dir is context.output_dir

    def test_project_spec_property(self, context: GeneratorContext) -> None:
        gen = _SimpleGenerator(context)
        assert gen.project_spec is context.project_spec

    def test_generator_config_property(self, context: GeneratorContext) -> None:
        gen = _SimpleGenerator(context)
        assert gen.generator_config is not None

    def test_generator_config_no_project_raises(
        self, basic_stack: StackSpec, tmp_path: Path
    ) -> None:
        ctx = GeneratorContext(domain_spec=basic_stack, output_dir=tmp_path)
        gen = _SimpleGenerator(ctx)
        with pytest.raises(ValueError, match="project_spec is required"):
            _ = gen.generator_config

    def test_exposure_config_property(self, context: GeneratorContext) -> None:
        gen = _SimpleGenerator(context)
        assert gen.exposure_config is not None

    def test_exposure_config_no_project(self, basic_stack: StackSpec, tmp_path: Path) -> None:
        ctx = GeneratorContext(domain_spec=basic_stack, output_dir=tmp_path)
        gen = _SimpleGenerator(ctx)
        assert gen.exposure_config is None

    def test_auth_config_property(self, context: GeneratorContext) -> None:
        gen = _SimpleGenerator(context)
        assert gen.auth_config is not None

    def test_auth_config_no_project_raises(self, basic_stack: StackSpec, tmp_path: Path) -> None:
        ctx = GeneratorContext(domain_spec=basic_stack, output_dir=tmp_path)
        gen = _SimpleGenerator(ctx)
        with pytest.raises(ValueError, match="project_spec is required"):
            _ = gen.auth_config

    def test_design_config_property(self, context: GeneratorContext) -> None:
        gen = _SimpleGenerator(context)
        assert gen.design_config is not None

    def test_design_config_no_project_raises(self, basic_stack: StackSpec, tmp_path: Path) -> None:
        ctx = GeneratorContext(domain_spec=basic_stack, output_dir=tmp_path)
        gen = _SimpleGenerator(ctx)
        with pytest.raises(ValueError, match="project_spec is required"):
            _ = gen.design_config

    def test_testing_config_property(self, context: GeneratorContext) -> None:
        gen = _SimpleGenerator(context)
        assert gen.testing_config is not None

    def test_testing_config_no_project_raises(self, basic_stack: StackSpec, tmp_path: Path) -> None:
        ctx = GeneratorContext(domain_spec=basic_stack, output_dir=tmp_path)
        gen = _SimpleGenerator(ctx)
        with pytest.raises(ValueError, match="project_spec is required"):
            _ = gen.testing_config

    def test_database_config_property(self, context: GeneratorContext) -> None:
        gen = _SimpleGenerator(context)
        assert gen.database_config is not None

    def test_database_config_no_project_raises(
        self, basic_stack: StackSpec, tmp_path: Path
    ) -> None:
        ctx = GeneratorContext(domain_spec=basic_stack, output_dir=tmp_path)
        gen = _SimpleGenerator(ctx)
        with pytest.raises(ValueError, match="project_spec is required"):
            _ = gen.database_config

    def test_get_package_name_from_context(self, context: GeneratorContext) -> None:
        context.backend_module_name = "my_module"
        gen = _SimpleGenerator(context)
        assert gen.get_package_name() == "my_module"

    def test_get_package_name_from_spec(self, context: GeneratorContext) -> None:
        gen = _SimpleGenerator(context)
        result = gen.get_package_name()
        assert result == "test_project"

    def test_render_string(self, context: GeneratorContext) -> None:
        gen = _SimpleGenerator(context)
        result = gen.render("Hello {{ name }}!", name="World")
        assert result == "Hello World!"

    def test_get_model_by_name_found(self, context: GeneratorContext) -> None:
        gen = _SimpleGenerator(context)
        model = gen.get_model_by_name("Item")
        assert model is not None
        assert model.name == "Item"

    def test_get_model_by_name_not_found(self, context: GeneratorContext) -> None:
        gen = _SimpleGenerator(context)
        model = gen.get_model_by_name("NonExistent")
        assert model is None

    def test_generate_writes_files(self, context: GeneratorContext) -> None:
        gen = _SimpleGenerator(context)
        result = gen.generate()
        assert result.success
        assert result.written == 1
        assert (context.output_dir / "output.py").exists()

    def test_generate_dry_run(self, dry_run_context: GeneratorContext) -> None:
        gen = _SimpleGenerator(dry_run_context)
        result = gen.generate()
        assert result.success
        assert not (dry_run_context.output_dir / "output.py").exists()

    def test_generate_failure(self, context: GeneratorContext) -> None:
        gen = _FailingGenerator(context)
        result = gen.generate()
        assert not result.success
        assert "generation failed" in result.errors[0]

    def test_generation_mode_default(self, context: GeneratorContext) -> None:
        gen = _SimpleGenerator(context)
        assert gen._generation_mode == "lenient"


class _SimpleModelGenerator(ModelGenerator):
    """Test model generator."""

    def generate_model_files(self, model: ModelSpec) -> list[GeneratedFile]:
        return [
            GeneratedFile(
                path=Path(f"{model.name.lower()}.py"),
                content=f"# {model.name}",
            )
        ]


class TestModelGenerator:
    """Tests for ModelGenerator."""

    def test_generates_per_model_files(self, context: GeneratorContext) -> None:
        gen = _SimpleModelGenerator(context)
        files = gen.generate_files()
        assert len(files) == 1
        assert files[0].path == Path("item.py")

    def test_shared_files_default_empty(self, context: GeneratorContext) -> None:
        gen = _SimpleModelGenerator(context)
        assert gen.generate_shared_files() == []

    def test_index_files_default_empty(self, context: GeneratorContext) -> None:
        gen = _SimpleModelGenerator(context)
        assert gen.generate_index_files() == []


class TestCompositeGenerator:
    """Tests for CompositeGenerator."""

    def test_no_generators(self, context: GeneratorContext) -> None:
        gen = CompositeGenerator(context)
        files = gen.generate_files()
        assert files == []

    def test_add_generator(self, context: GeneratorContext) -> None:
        composite = CompositeGenerator(context)
        sub = _SimpleGenerator(context)
        composite.add_generator(sub)
        assert len(composite.generators) == 1

    def test_generates_from_all_subgenerators(self, context: GeneratorContext) -> None:
        composite = CompositeGenerator(context)
        composite.add_generator(_SimpleGenerator(context))
        composite.add_generator(_SimpleGenerator(context))
        files = composite.generate_files()
        assert len(files) == 2

    def test_generate_combines_results(self, dry_run_context: GeneratorContext) -> None:
        composite = CompositeGenerator(dry_run_context)
        composite.add_generator(_SimpleGenerator(dry_run_context))
        composite.add_generator(_SimpleGenerator(dry_run_context))
        result = composite.generate()
        assert result.success
        assert len(result.files) == 2


class TestCreateInitFile:
    """Tests for create_init_file."""

    def test_basic(self) -> None:
        gf = create_init_file(
            module_path=Path("app/models"),
            imports=["from .user import User"],
            all_exports=["User"],
        )
        assert gf.path == Path("app/models/__init__.py")
        assert "from .user import User" in gf.content
        assert '"User"' in gf.content

    def test_with_description(self) -> None:
        gf = create_init_file(
            module_path=Path("app"),
            imports=[],
            all_exports=[],
            description="App module.",
        )
        assert '"""App module."""' in gf.content

    def test_with_future_annotations(self) -> None:
        gf = create_init_file(
            module_path=Path("app"),
            imports=[],
            all_exports=[],
            use_future_annotations=True,
        )
        assert "from __future__ import annotations" in gf.content

    def test_empty_exports(self) -> None:
        gf = create_init_file(
            module_path=Path("app"),
            imports=[],
            all_exports=[],
        )
        assert "__all__: list[str] = []" in gf.content

    def test_sorted_exports(self) -> None:
        gf = create_init_file(
            module_path=Path("app"),
            imports=[],
            all_exports=["Zebra", "Alpha", "Middle"],
        )
        content = gf.content
        alpha_idx = content.index('"Alpha"')
        middle_idx = content.index('"Middle"')
        zebra_idx = content.index('"Zebra"')
        assert alpha_idx < middle_idx < zebra_idx

    def test_strategy_is_always_overwrite(self) -> None:
        gf = create_init_file(
            module_path=Path("app"),
            imports=[],
            all_exports=[],
        )
        assert gf.strategy == FileStrategy.ALWAYS_OVERWRITE
