# allow: no-sut-import — tests scripts/generate_config_docs.py, repo tooling outside the local_deep_research package
"""
Tests for scripts/generate_config_docs.py

Validates that the configuration documentation generator correctly:
- Converts setting keys to env var names
- Formats default values for markdown
- Auto-discovers all env_definitions modules
- Extracts all fields from Setting constructors
- Generates valid markdown covering all JSON files and env_definitions
- Detects stale documentation via --check mode
- Handles errors gracefully
"""

import json
import sys
from pathlib import Path


# Ensure the scripts directory is importable
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
SCRIPTS_DIR = REPO_ROOT / "scripts"
sys.path.insert(0, str(SCRIPTS_DIR))

from generate_config_docs import (  # noqa: E402
    format_value,
    generate_docs,
    generate_docs_content,
    get_env_only_settings,
    get_env_var_name,
)

# ── Paths ──────────────────────────────────────────────────────────────
DEFAULTS_DIR = REPO_ROOT / "src" / "local_deep_research" / "defaults"
ENV_DEFS_DIR = (
    REPO_ROOT / "src" / "local_deep_research" / "settings" / "env_definitions"
)
OUTPUT_FILE = REPO_ROOT / "docs" / "CONFIGURATION.md"

# Expected env_definitions modules (excluding __init__.py and env_settings.py)
EXPECTED_ENV_MODULES = {
    "bootstrap",
    "db_config",
    "news_scheduler",
    "security",
    "testing",
}


# ═══════════════════════════════════════════════════════════════════════
# Unit tests
# ═══════════════════════════════════════════════════════════════════════


class TestGetEnvVarName:
    """Test dotted keys -> LDR_* env var conversion."""

    def test_simple_key(self):
        assert get_env_var_name("app.debug") == "LDR_APP_DEBUG"

    def test_nested_key(self):
        assert get_env_var_name("llm.openai.model") == "LDR_LLM_OPENAI_MODEL"

    def test_already_uppercase(self):
        assert get_env_var_name("DB.HOST") == "LDR_DB_HOST"

    def test_single_segment(self):
        assert get_env_var_name("port") == "LDR_PORT"


class TestFormatValue:
    """Test value formatting for markdown table cells."""

    def test_none(self):
        assert format_value(None) == "null"

    def test_bool_true(self):
        assert format_value(True) == "true"

    def test_bool_false(self):
        assert format_value(False) == "false"

    def test_dict(self):
        result = format_value({"a": 1})
        assert result.startswith("`")
        assert '"a"' in result

    def test_list(self):
        result = format_value([1, 2])
        assert result.startswith("`")
        assert "[1, 2]" in result

    def test_string(self):
        assert format_value("hello") == "hello"

    def test_int(self):
        assert format_value(42) == "42"


class TestGetEnvOnlySettings:
    """Test env_definitions AST extraction."""

    def test_returns_all_modules(self):
        """All 5 env_definitions modules should be represented."""
        settings = get_env_only_settings()
        categories = {s["category"].lower().replace(" ", "_") for s in settings}
        for module in EXPECTED_ENV_MODULES:
            # Category is derived from filename: db_config.py -> "Db Config" -> "db_config"
            assert module in categories, (
                f"Module '{module}' not found in categories: {categories}"
            )

    def test_extracts_all_fields(self):
        """Each setting should have the core fields populated."""
        settings = get_env_only_settings()
        assert len(settings) > 0

        required_fields = {
            "key",
            "env_var",
            "description",
            "default",
            "type",
            "required",
            "min_value",
            "max_value",
            "allowed_values",
            "deprecated_env_var",
        }
        for s in settings:
            missing = required_fields - set(s.keys())
            assert not missing, (
                f"Setting '{s.get('key', '?')}' is missing fields: {missing}"
            )

    def test_bootstrap_encryption_key_present(self):
        """The bootstrap encryption key should be discovered."""
        settings = get_env_only_settings()
        keys = {s["key"] for s in settings}
        assert "bootstrap.encryption_key" in keys

    def test_db_config_has_constraints(self):
        """db_config settings should have min/max values extracted."""
        settings = get_env_only_settings()
        cache_size = next(
            (s for s in settings if s["key"] == "db_config.cache_size_mb"),
            None,
        )
        assert cache_size is not None, "db_config.cache_size_mb not found"
        assert cache_size["min_value"] == 1
        assert cache_size["max_value"] == 10000

    def test_db_config_has_allowed_values(self):
        """Enum settings should have allowed_values extracted."""
        settings = get_env_only_settings()
        journal = next(
            (s for s in settings if s["key"] == "db_config.journal_mode"),
            None,
        )
        assert journal is not None, "db_config.journal_mode not found"
        assert journal["allowed_values"] is not None
        assert "WAL" in journal["allowed_values"]

    def test_deprecated_env_var_extracted(self):
        """Settings with deprecated_env_var should have it extracted."""
        settings = get_env_only_settings()
        cache_size = next(
            (s for s in settings if s["key"] == "db_config.cache_size_mb"),
            None,
        )
        assert cache_size is not None
        assert cache_size["deprecated_env_var"] == "LDR_DB_CACHE_SIZE_MB"


# ═══════════════════════════════════════════════════════════════════════
# Integration tests
# ═══════════════════════════════════════════════════════════════════════


class TestGenerateDocsIntegration:
    """Test the full generation pipeline."""

    def test_produces_valid_markdown(self):
        """Generated content should have the expected markdown structure."""
        content = generate_docs_content()
        assert content.startswith("# Configuration Reference\n")
        assert "## Pre-Database (Env-Only) Settings" in content
        assert "## Settings List" in content
        assert "| Key | Environment Variable |" in content
        assert "*Generated by scripts/generate_config_docs.py*" in content

    def test_includes_all_json_files(self):
        """All JSON config files should contribute settings to the output."""
        json_files = sorted(DEFAULTS_DIR.rglob("*.json"))
        assert len(json_files) >= 18, (
            f"Expected at least 18 JSON files, found {len(json_files)}"
        )

        content = generate_docs_content()

        # Collect all keys from all JSON files
        all_json_keys = set()
        for jf in json_files:
            try:
                data = json.loads(jf.read_text())
                all_json_keys.update(data.keys())
            except Exception:
                continue

        # Every key should appear in the generated doc
        for key in all_json_keys:
            assert f"`{key}`" in content, (
                f"Setting key '{key}' not found in generated docs"
            )

    def test_includes_all_env_definitions(self):
        """All env_definitions modules should be represented."""
        content = generate_docs_content()
        settings = get_env_only_settings()

        for s in settings:
            assert s["env_var"] in content, (
                f"Env var '{s['env_var']}' not found in generated docs"
            )

    def test_no_env_var_collisions(self):
        """No duplicate env var names should exist across all settings."""
        settings = get_env_only_settings()
        env_vars = [s["env_var"] for s in settings]
        duplicates = {v for v in env_vars if env_vars.count(v) > 1}
        assert not duplicates, f"Duplicate env var names found: {duplicates}"

    def test_env_only_table_has_expanded_columns(self):
        """The env-only table should include Type, Required, Constraints columns."""
        content = generate_docs_content()
        header_line = None
        for line in content.split("\n"):
            if (
                "Environment Variable" in line
                and "Type" in line
                and "Required" in line
            ):
                header_line = line
                break

        assert header_line is not None, (
            "Could not find expanded env-only table header"
        )
        assert "Constraints" in header_line
        assert "Deprecated Alias" in header_line


# ═══════════════════════════════════════════════════════════════════════
# Check mode tests
# ═══════════════════════════════════════════════════════════════════════


class TestCheckMode:
    """Test the --check flag behaviour."""

    def test_check_mode_passes_when_fresh(self, tmp_path):
        """Exit 0 when docs match the generated output."""
        out = tmp_path / "CONFIGURATION.md"
        # Generate fresh
        assert generate_docs(output_path=out, check=False) == 0
        # Check should pass
        assert generate_docs(output_path=out, check=True) == 0

    def test_check_mode_fails_when_stale(self, tmp_path):
        """Exit 1 when docs differ from the generated output."""
        out = tmp_path / "CONFIGURATION.md"
        # Generate, then tamper
        generate_docs(output_path=out, check=False)
        out.write_text("stale content\n")
        assert generate_docs(output_path=out, check=True) == 1

    def test_check_mode_fails_when_missing(self, tmp_path):
        """Exit 1 when the docs file doesn't exist."""
        out = tmp_path / "CONFIGURATION.md"
        assert generate_docs(output_path=out, check=True) == 1


# ═══════════════════════════════════════════════════════════════════════
# Error handling tests
# ═══════════════════════════════════════════════════════════════════════


class TestErrorHandling:
    """Test graceful handling of bad inputs."""

    def test_handles_missing_json_gracefully(self, tmp_path):
        """Corrupted/missing JSON in defaults dir doesn't crash generation."""
        # Create a minimal project structure with a bad JSON file
        defaults = tmp_path / "src" / "local_deep_research" / "defaults"
        defaults.mkdir(parents=True)
        (defaults / "bad.json").write_text("{invalid json")
        (defaults / "good.json").write_text(
            json.dumps(
                {
                    "test.key": {
                        "value": "hello",
                        "description": "A test",
                        "type": "STRING",
                    }
                }
            )
        )

        # Also create empty env_definitions dir
        env_defs = (
            tmp_path
            / "src"
            / "local_deep_research"
            / "settings"
            / "env_definitions"
        )
        env_defs.mkdir(parents=True)

        content = generate_docs_content(root_dir=tmp_path)
        # Good file should still be included
        assert "`test.key`" in content

    def test_handles_ast_parse_errors_gracefully(self, tmp_path):
        """Bad Python in env_definitions doesn't crash generation."""
        env_defs = (
            tmp_path
            / "src"
            / "local_deep_research"
            / "settings"
            / "env_definitions"
        )
        env_defs.mkdir(parents=True)
        (env_defs / "broken.py").write_text("def incomplete(")

        defaults = tmp_path / "src" / "local_deep_research" / "defaults"
        defaults.mkdir(parents=True)

        # Should not raise
        settings = get_env_only_settings(root_dir=tmp_path)
        assert isinstance(settings, list)
