"""
Tests for the configuration system (Milestone 4).
"""

from pathlib import Path

import pytest

from nblite.config import (
    CellReferenceStyle,
    CleanConfig,
    CodeLocationConfig,
    CodeLocationFormat,
    ConfigError,
    DocsConfig,
    ExportConfig,
    ExportMode,
    ExportRule,
    ExtensionEntry,
    GitConfig,
    NbliteConfig,
    TemplatesConfig,
    find_config_file,
    load_config,
    parse_export_pipeline,
)


class TestExportRule:
    def test_create_export_rule(self) -> None:
        """Test creating an export rule."""
        rule = ExportRule(from_key="nbs", to_key="pts")
        assert rule.from_key == "nbs"
        assert rule.to_key == "pts"

    def test_export_rule_equality(self) -> None:
        """Test export rule equality."""
        rule1 = ExportRule(from_key="nbs", to_key="pts")
        rule2 = ExportRule(from_key="nbs", to_key="pts")
        assert rule1 == rule2


class TestCodeLocationConfig:
    def test_create_code_location(self) -> None:
        """Test creating a code location config."""
        cl = CodeLocationConfig(
            path="mypackage",
            format=CodeLocationFormat.MODULE,
            export_mode=ExportMode.PERCENT,
        )
        assert cl.path == "mypackage"
        assert cl.format == CodeLocationFormat.MODULE

    def test_code_location_defaults(self) -> None:
        """Test code location default values."""
        cl = CodeLocationConfig(path="nbs", format=CodeLocationFormat.IPYNB)
        assert cl.export_mode == ExportMode.PERCENT

    def test_code_location_all_formats(self) -> None:
        """Test all valid format types."""
        for fmt in CodeLocationFormat:
            cl = CodeLocationConfig(path="test", format=fmt)
            assert cl.format == fmt

    def test_export_modes(self) -> None:
        """Test export modes for module format."""
        for mode in ExportMode:
            cl = CodeLocationConfig(
                path="lib",
                format=CodeLocationFormat.MODULE,
                export_mode=mode,
            )
            assert cl.export_mode == mode


class TestTemplatesConfig:
    def test_templates_defaults(self) -> None:
        """Test templates default values."""
        tc = TemplatesConfig()
        assert tc.folder == "templates"
        assert tc.default is None

    def test_templates_custom(self) -> None:
        """Test custom templates config."""
        tc = TemplatesConfig(folder="my_templates", default="api.ipynb")
        assert tc.folder == "my_templates"
        assert tc.default == "api.ipynb"


class TestExtensionEntry:
    def test_extension_with_path(self) -> None:
        """Test extension entry with file path."""
        ext = ExtensionEntry(path="nblite_ext.py")
        assert ext.path == "nblite_ext.py"
        assert ext.module is None

    def test_extension_with_module(self) -> None:
        """Test extension entry with module path."""
        ext = ExtensionEntry(module="mypackage.extension")
        assert ext.module == "mypackage.extension"
        assert ext.path is None

    def test_extension_neither_raises(self) -> None:
        """Test neither path nor module raises error."""
        with pytest.raises(ValueError, match="Either 'path' or 'module'"):
            ExtensionEntry()

    def test_extension_both_raises(self) -> None:
        """Test both path and module raises error."""
        with pytest.raises(ValueError, match="Cannot specify both"):
            ExtensionEntry(path="ext.py", module="pkg.ext")


class TestExportConfig:
    def test_export_config_defaults(self) -> None:
        """Test export config default values."""
        ec = ExportConfig()
        assert ec.include_autogenerated_warning is True
        assert ec.cell_reference_style == CellReferenceStyle.RELATIVE

    def test_export_config_custom(self) -> None:
        """Test custom export config."""
        ec = ExportConfig(
            include_autogenerated_warning=False,
            cell_reference_style=CellReferenceStyle.ABSOLUTE,
        )
        assert ec.include_autogenerated_warning is False
        assert ec.cell_reference_style == CellReferenceStyle.ABSOLUTE


class TestGitConfig:
    def test_git_config_defaults(self) -> None:
        """Test git config default values."""
        gc = GitConfig()
        assert gc.auto_clean is True
        assert gc.auto_export is True
        assert gc.validate_staging is True

    def test_git_config_custom(self) -> None:
        """Test custom git config."""
        gc = GitConfig(
            auto_clean=False,
            auto_export=False,
            validate_staging=False,
        )
        assert gc.auto_clean is False
        assert gc.auto_export is False
        assert gc.validate_staging is False


class TestCleanConfig:
    def test_clean_config_defaults(self) -> None:
        """Test clean config default values (based on for_vcs() defaults)."""
        cc = CleanConfig()
        # Defaults based on notebookx.CleanOptions.for_vcs() for VCS-friendly cleaning
        assert cc.remove_outputs is False  # Keep outputs
        assert cc.remove_execution_counts is True  # Remove for clean diffs
        assert cc.remove_cell_metadata is True  # Remove for clean diffs
        assert cc.remove_notebook_metadata is False  # Keep notebook metadata
        assert cc.remove_kernel_info is False  # Keep kernel info
        assert cc.preserve_cell_ids is True  # Preserve for idempotency
        assert cc.remove_output_metadata is True  # Remove for clean diffs
        assert cc.remove_output_execution_counts is True  # Remove for clean diffs
        assert cc.keep_only_metadata is None
        # Exclusion options still default to True
        assert cc.exclude_dunders is True
        assert cc.exclude_hidden is True


class TestDocsConfig:
    def test_docs_config_defaults(self) -> None:
        """Test docs config default values."""
        dc = DocsConfig()
        assert dc.output_folder == "_docs"
        assert dc.execute_notebooks is False
        assert dc.exclude_patterns == ["__*", ".*"]


class TestNbliteConfig:
    def test_minimal_config(self) -> None:
        """Test minimal valid config."""
        config = NbliteConfig(
            export_pipeline=[ExportRule(from_key="nbs", to_key="lib")],
            code_locations={
                "nbs": CodeLocationConfig(path="nbs", format=CodeLocationFormat.IPYNB),
                "lib": CodeLocationConfig(path="mypackage", format=CodeLocationFormat.MODULE),
            },
        )
        assert len(config.export_pipeline) == 1
        assert len(config.code_locations) == 2

    def test_config_defaults(self) -> None:
        """Test config defaults are applied."""
        config = NbliteConfig(export_pipeline=[], code_locations={})
        assert config.docs_generator == "mkdocs"
        assert config.export.include_autogenerated_warning is True
        assert config.git.auto_clean is True

    def test_config_with_extensions(self) -> None:
        """Test config with multiple extensions."""
        config = NbliteConfig(
            export_pipeline=[],
            code_locations={},
            extensions=[
                ExtensionEntry(path="ext1.py"),
                ExtensionEntry(module="pkg.ext2"),
            ],
        )
        assert len(config.extensions) == 2

    def test_config_with_docs_settings(self) -> None:
        """Test config with documentation settings."""
        config = NbliteConfig(
            export_pipeline=[],
            code_locations={"nbs": CodeLocationConfig(path="nbs", format=CodeLocationFormat.IPYNB)},
            docs_cl="nbs",
            docs_title="My Package",
            docs_generator="mkdocs",
        )
        assert config.docs_cl == "nbs"
        assert config.docs_title == "My Package"
        assert config.docs_generator == "mkdocs"


class TestParseExportPipeline:
    def test_parse_simple_pipeline(self) -> None:
        """Test parsing simple pipeline string."""
        pipeline_str = "nbs -> pts"
        rules = parse_export_pipeline(pipeline_str)
        assert len(rules) == 1
        assert rules[0].from_key == "nbs"
        assert rules[0].to_key == "pts"

    def test_parse_multiline_pipeline(self) -> None:
        """Test parsing multi-line pipeline."""
        pipeline_str = """
        nbs -> pts
        pts -> lib
        """
        rules = parse_export_pipeline(pipeline_str)
        assert len(rules) == 2
        assert rules[0].from_key == "nbs"
        assert rules[1].to_key == "lib"

    def test_parse_empty_pipeline(self) -> None:
        """Test parsing empty pipeline."""
        rules = parse_export_pipeline("")
        assert len(rules) == 0

    def test_parse_pipeline_with_comments(self) -> None:
        """Test parsing pipeline with comments."""
        pipeline_str = """
        # This is a comment
        nbs -> pts
        # Another comment
        pts -> lib
        """
        rules = parse_export_pipeline(pipeline_str)
        assert len(rules) == 2

    def test_parse_pipeline_with_whitespace(self) -> None:
        """Test parsing pipeline with extra whitespace."""
        rules = parse_export_pipeline("  nbs   ->   pts  ")
        assert len(rules) == 1
        assert rules[0].from_key == "nbs"
        assert rules[0].to_key == "pts"


class TestConfigLoader:
    def test_load_config_from_file(self, tmp_path: Path) -> None:
        """Test loading config from TOML file."""
        config_content = """
export_pipeline = "nbs -> lib"

[cl.nbs]
path = "nbs"
format = "ipynb"

[cl.lib]
path = "mypackage"
format = "module"
"""
        config_path = tmp_path / "nblite.toml"
        config_path.write_text(config_content)
        config = load_config(config_path)
        assert config is not None
        assert len(config.export_pipeline) == 1
        assert "nbs" in config.code_locations
        assert "lib" in config.code_locations

    def test_load_config_with_all_sections(self, tmp_path: Path) -> None:
        """Test loading config with all optional sections."""
        config_content = """
export_pipeline = "nbs -> lib"
docs_cl = "nbs"
docs_title = "Test Package"
docs_generator = "mkdocs"

[cl.nbs]
path = "nbs"
format = "ipynb"

[cl.lib]
path = "mypackage"
format = "module"
export_mode = "py"

[templates]
folder = "my_templates"
default = "default.ipynb"

[[extensions]]
path = "ext.py"

[export]
include_autogenerated_warning = false
cell_reference_style = "absolute"

[git]
auto_clean = false
auto_export = false
validate_staging = false

[clean]
remove_outputs = true
remove_cell_metadata = true

[docs]
output_folder = "docs_output"
execute_notebooks = true
"""
        config_path = tmp_path / "nblite.toml"
        config_path.write_text(config_content)
        config = load_config(config_path)

        assert config.docs_cl == "nbs"
        assert config.docs_title == "Test Package"
        assert config.docs_generator == "mkdocs"
        assert config.templates.folder == "my_templates"
        assert config.templates.default == "default.ipynb"
        assert len(config.extensions) == 1
        assert config.export.include_autogenerated_warning is False
        assert config.git.auto_clean is False
        assert config.clean.remove_outputs is True
        assert config.clean.remove_cell_metadata is True
        assert config.docs.output_folder == "docs_output"
        assert config.code_locations["lib"].export_mode == ExportMode.PY

    def test_find_config_file(self, tmp_path: Path) -> None:
        """Test finding config file by searching upward."""
        # Create nested directory structure
        nested = tmp_path / "a" / "b" / "c"
        nested.mkdir(parents=True)
        config_path = tmp_path / "nblite.toml"
        config_path.write_text('export_pipeline = ""')

        found = find_config_file(nested)
        assert found == config_path

    def test_find_config_file_not_found(self, tmp_path: Path) -> None:
        """Test config file not found returns None."""
        found = find_config_file(tmp_path)
        assert found is None

    def test_find_config_file_in_same_dir(self, tmp_path: Path) -> None:
        """Test finding config file in same directory."""
        config_path = tmp_path / "nblite.toml"
        config_path.write_text('export_pipeline = ""')

        found = find_config_file(tmp_path)
        assert found == config_path

    def test_load_config_validates_code_locations(self, tmp_path: Path) -> None:
        """Test that config validates pipeline references."""
        config_content = """
export_pipeline = "nbs -> nonexistent"

[cl.nbs]
path = "nbs"
format = "ipynb"
"""
        config_path = tmp_path / "nblite.toml"
        config_path.write_text(config_content)
        with pytest.raises(ConfigError, match="nonexistent"):
            load_config(config_path)

    def test_load_config_validates_docs_cl(self, tmp_path: Path) -> None:
        """Test that config validates docs_cl reference."""
        config_content = """
export_pipeline = ""
docs_cl = "nonexistent"

[cl.nbs]
path = "nbs"
format = "ipynb"
"""
        config_path = tmp_path / "nblite.toml"
        config_path.write_text(config_content)
        with pytest.raises(ConfigError, match="nonexistent"):
            load_config(config_path)

    def test_load_config_file_not_found(self, tmp_path: Path) -> None:
        """Test loading non-existent config raises error."""
        with pytest.raises(FileNotFoundError):
            load_config(tmp_path / "nonexistent.toml")

    def test_load_config_invalid_toml(self, tmp_path: Path) -> None:
        """Test loading invalid TOML raises error."""
        config_path = tmp_path / "nblite.toml"
        config_path.write_text("invalid = [")
        with pytest.raises(ConfigError, match="Invalid TOML"):
            load_config(config_path)

    def test_load_config_invalid_format(self, tmp_path: Path) -> None:
        """Test loading config with invalid format raises error."""
        config_content = """
export_pipeline = ""

[cl.nbs]
path = "nbs"
format = "invalid_format"
"""
        config_path = tmp_path / "nblite.toml"
        config_path.write_text(config_content)
        with pytest.raises(ConfigError, match="Invalid format"):
            load_config(config_path)

    def test_load_config_circular_reference(self, tmp_path: Path) -> None:
        """Test loading config with circular export pipeline raises error."""
        config_content = '''
export_pipeline = """
nbs -> lib
lib -> nbs
"""

[cl.nbs]
path = "nbs"
format = "ipynb"

[cl.lib]
path = "lib"
format = "module"
'''
        config_path = tmp_path / "nblite.toml"
        config_path.write_text(config_content)
        with pytest.raises(ConfigError, match="Circular reference"):
            load_config(config_path)

    def test_load_config_longer_circular_reference(self, tmp_path: Path) -> None:
        """Test loading config with longer circular chain raises error."""
        config_content = '''
export_pipeline = """
nbs -> pcts
pcts -> lib
lib -> nbs
"""

[cl.nbs]
path = "nbs"
format = "ipynb"

[cl.pcts]
path = "pcts"
format = "percent"

[cl.lib]
path = "lib"
format = "module"
'''
        config_path = tmp_path / "nblite.toml"
        config_path.write_text(config_content)
        with pytest.raises(ConfigError, match="Circular reference"):
            load_config(config_path)
