"""Tests for configuration loading and API key resolution.

Tests load_config, resolve_api_key, get_campaign_dir from config.py.
Uses tmp_path for config files, monkeypatch for env vars and home directory.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

from unfamiliar.config import (
    APIKeyError,
    ConfigError,
    UnfamiliarConfig,
    get_campaign_dir,
    load_config,
    resolve_api_key,
)

if TYPE_CHECKING:
    from pathlib import Path


class TestLoadConfig:
    """Tests for load_config function."""

    def test_loads_defaults_when_file_missing(self, tmp_path: Path):
        """Missing config file returns defaults and creates the file."""
        config_path = tmp_path / "config.toml"

        config = load_config(config_path)

        assert config.provider == "anthropic"
        assert config.token_budget == 50000
        assert config.theme == "film_noir"
        assert config_path.exists()

    def test_loads_from_toml_file(self, tmp_path: Path):
        """Config is loaded from TOML file."""
        config_path = tmp_path / "config.toml"
        config_path.write_text("""
[general]
provider = "glm"
token_budget = 100000
theme = "cyberpunk"
""")

        config = load_config(config_path)

        assert config.provider == "glm"
        assert config.token_budget == 100000
        assert config.theme == "cyberpunk"

    def test_empty_file_returns_defaults(self, tmp_path: Path):
        """Empty config file returns defaults."""
        config_path = tmp_path / "config.toml"
        config_path.write_text("")

        config = load_config(config_path)

        assert config.provider == "anthropic"

    def test_whitespace_only_file_returns_defaults(self, tmp_path: Path):
        """Config file with only whitespace returns defaults."""
        config_path = tmp_path / "config.toml"
        config_path.write_text("   \n\n   ")

        config = load_config(config_path)

        assert config.provider == "anthropic"

    def test_invalid_toml_raises_config_error(self, tmp_path: Path):
        """Invalid TOML raises ConfigError."""
        config_path = tmp_path / "config.toml"
        config_path.write_text("not valid toml [[[")

        with pytest.raises(ConfigError) as exc_info:
            load_config(config_path)

        assert "Invalid TOML in" in str(exc_info.value)

    def test_creates_parent_directory(self, tmp_path: Path):
        """Parent directory is created if it doesn't exist."""
        config_path = tmp_path / "subdir" / "config.toml"
        assert not config_path.parent.exists()

        load_config(config_path)

        assert config_path.parent.exists()

    def test_api_keys_section_in_config_file_is_ignored(self, tmp_path: Path):
        """API keys in config file are ignored (env vars only)."""
        config_path = tmp_path / "config.toml"
        config_path.write_text("""
[general]
provider = "anthropic"

[api_keys]
anthropic = "sk-test-key"
glm = "glm-test-key"
""")

        config = load_config(config_path)

        # api_keys section is ignored - env vars only for API keys
        assert config.provider == "anthropic"


class TestResolveApiKey:
    """Tests for resolve_api_key function."""

    def test_provider_env_var_takes_precedence(self, monkeypatch):
        """Provider-specific env var is used first."""
        monkeypatch.setenv("ANTHROPIC_API_KEY", "provider-key")
        monkeypatch.setenv("UNFAMILIAR_API_KEY", "generic-key")
        config = UnfamiliarConfig()

        result = resolve_api_key("anthropic", config)

        assert result == "provider-key"

    def test_generic_env_var_used_if_provider_var_missing(self, monkeypatch):
        """Generic env var is used when provider-specific is missing."""
        monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
        monkeypatch.setenv("UNFAMILIAR_API_KEY", "generic-key")
        config = UnfamiliarConfig()

        result = resolve_api_key("anthropic", config)

        assert result == "generic-key"

    def test_raises_api_key_error_when_no_env_vars(self, monkeypatch):
        """APIKeyError raised when no env vars are set (config file no longer used)."""
        monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
        monkeypatch.delenv("UNFAMILIAR_API_KEY", raising=False)
        config = UnfamiliarConfig()

        with pytest.raises(APIKeyError) as exc_info:
            resolve_api_key("anthropic", config)

        assert "No API key found" in str(exc_info.value)
        assert "ANTHROPIC_API_KEY" in str(exc_info.value)

    def test_raises_api_key_error_when_not_found(self, monkeypatch):
        """APIKeyError raised when no key is available."""
        monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
        monkeypatch.delenv("UNFAMILIAR_API_KEY", raising=False)
        config = UnfamiliarConfig()

        with pytest.raises(APIKeyError) as exc_info:
            resolve_api_key("anthropic", config)

        assert "No API key found" in str(exc_info.value)
        assert "ANTHROPIC_API_KEY" in str(exc_info.value)

    def test_handles_unknown_provider(self, monkeypatch):
        """Unknown provider checks generic env var and config."""
        monkeypatch.setenv("UNFAMILIAR_API_KEY", "generic-key")
        config = UnfamiliarConfig()

        result = resolve_api_key("unknown_provider", config)

        assert result == "generic-key"


class TestGetCampaignDir:
    """Tests for get_campaign_dir function."""

    def test_returns_path_with_repo_name_and_hash(self, tmp_path: Path):
        """Campaign dir has format <repo_name>-<hash>."""
        base_dir = tmp_path / ".unfamiliar"

        repo_path = tmp_path / "myproject"
        repo_path.mkdir()

        result = get_campaign_dir(repo_path, base_dir=base_dir)

        assert result.parent.name == "campaigns"
        assert result.name.startswith("myproject-")
        # Hash is 6 hex characters
        parts = result.name.split("-")
        assert len(parts[-1]) == 6

    def test_hash_is_deterministic(self, tmp_path: Path):
        """Same repo path always produces same hash."""
        base_dir = tmp_path / ".unfamiliar"

        repo_path = tmp_path / "myproject"
        repo_path.mkdir()

        result1 = get_campaign_dir(repo_path, base_dir=base_dir)
        result2 = get_campaign_dir(repo_path, base_dir=base_dir)

        assert result1 == result2

    def test_different_repos_produce_different_dirs(self, tmp_path: Path):
        """Different repo paths produce different campaign directories."""
        base_dir = tmp_path / ".unfamiliar"

        repo_a = tmp_path / "project_a"
        repo_a.mkdir()
        repo_b = tmp_path / "project_b"
        repo_b.mkdir()

        result_a = get_campaign_dir(repo_a, base_dir=base_dir)
        result_b = get_campaign_dir(repo_b, base_dir=base_dir)

        assert result_a != result_b

    def test_creates_campaigns_directory(self, tmp_path: Path):
        """Campaigns directory is created if it doesn't exist."""
        base_dir = tmp_path / ".unfamiliar"

        repo_path = tmp_path / "myproject"
        repo_path.mkdir()

        get_campaign_dir(repo_path, base_dir=base_dir)

        assert (base_dir / "campaigns").exists()

    def test_reuses_existing_dir_with_matching_marker(self, tmp_path: Path):
        """Existing dir with matching .repo_path marker is reused."""
        base_dir = tmp_path / ".unfamiliar"

        repo_path = tmp_path / "myproject"
        repo_path.mkdir()

        # First call creates the dir
        result1 = get_campaign_dir(repo_path, base_dir=base_dir)

        # Create marker file
        result1.mkdir(parents=True, exist_ok=True)
        (result1 / ".repo_path").write_text(str(repo_path.resolve()))

        # Second call should return same dir
        result2 = get_campaign_dir(repo_path, base_dir=base_dir)

        assert result1 == result2


class TestExceptions:
    """Tests for exception classes."""

    def test_config_error_is_exception(self):
        """ConfigError is an Exception subclass."""
        assert issubclass(ConfigError, Exception)

    def test_api_key_error_is_exception(self):
        """APIKeyError is an Exception subclass."""
        assert issubclass(APIKeyError, Exception)

    def test_config_error_message(self):
        """ConfigError preserves message."""
        exc = ConfigError("test message")
        assert str(exc) == "test message"

    def test_api_key_error_message(self):
        """APIKeyError preserves message."""
        exc = APIKeyError("no key found")
        assert str(exc) == "no key found"


class TestGetRepoConfigPath:
    """Tests for get_repo_config_path function."""

    def test_returns_path_with_repos_and_hash(self, tmp_path: Path):
        """get_repo_config_path returns path under repos/ with 8-char hash."""
        from unfamiliar.config import get_repo_config_path

        repo_path = tmp_path / "myproject"
        repo_path.mkdir()

        result = get_repo_config_path(repo_path, base_dir=tmp_path)

        assert result.parent.name == "repos"
        assert len(result.name) == 8

    def test_same_repo_path_produces_same_hash(self, tmp_path: Path):
        """Same repo path always produces the same config path hash."""
        from unfamiliar.config import get_repo_config_path

        repo_path = tmp_path / "myproject"
        repo_path.mkdir()

        result1 = get_repo_config_path(repo_path, base_dir=tmp_path)
        result2 = get_repo_config_path(repo_path, base_dir=tmp_path)

        assert result1 == result2

    def test_different_repo_paths_produce_different_hashes(self, tmp_path: Path):
        """Different repo paths produce different config directory hashes."""
        from unfamiliar.config import get_repo_config_path

        repo_a = tmp_path / "project_a"
        repo_a.mkdir()
        repo_b = tmp_path / "project_b"
        repo_b.mkdir()

        result_a = get_repo_config_path(repo_a, base_dir=tmp_path)
        result_b = get_repo_config_path(repo_b, base_dir=tmp_path)

        assert result_a != result_b

    def test_uses_default_base_dir_when_none(self, tmp_path: Path, monkeypatch):
        """Uses ~/.unfamiliar as base_dir when None is passed."""
        from unfamiliar.config import get_repo_config_path

        repo_path = tmp_path / "myproject"
        repo_path.mkdir()

        monkeypatch.setenv("HOME", str(tmp_path))

        result = get_repo_config_path(repo_path, base_dir=None)

        assert ".unfamiliar" in str(result)
        assert "repos" in str(result)


class TestLoadRepoFilterConfig:
    """Tests for load_repo_filter_config function."""

    def test_missing_config_returns_empty_config(self, tmp_path: Path):
        """Missing config file returns RepoFilterConfig with empty lists."""
        from unfamiliar.config import RepoFilterConfig, load_repo_filter_config

        repo_path = tmp_path / "myproject"
        repo_path.mkdir()

        result = load_repo_filter_config(repo_path, base_dir=tmp_path)

        assert isinstance(result, RepoFilterConfig)
        assert result.include_patterns == []
        assert result.exclude_patterns == []

    def test_empty_config_file_returns_empty_config(self, tmp_path: Path):
        """Empty config.yaml returns RepoFilterConfig with empty lists."""
        from unfamiliar.config import (
            RepoFilterConfig,
            get_repo_config_path,
            load_repo_filter_config,
        )

        repo_path = tmp_path / "myproject"
        repo_path.mkdir()

        config_dir = get_repo_config_path(repo_path, base_dir=tmp_path)
        config_dir.mkdir(parents=True)
        (config_dir / "config.yaml").write_text("")

        result = load_repo_filter_config(repo_path, base_dir=tmp_path)

        assert isinstance(result, RepoFilterConfig)
        assert result.include_patterns == []
        assert result.exclude_patterns == []

    def test_valid_config_with_patterns_returns_config(self, tmp_path: Path):
        """Valid config.yaml with patterns returns populated RepoFilterConfig."""
        from unfamiliar.config import (
            get_repo_config_path,
            load_repo_filter_config,
        )

        repo_path = tmp_path / "myproject"
        repo_path.mkdir()

        config_dir = get_repo_config_path(repo_path, base_dir=tmp_path)
        config_dir.mkdir(parents=True)
        (config_dir / "config.yaml").write_text("""
include_patterns:
  - "src/**/*.py"
  - "lib/**/*.py"
exclude_patterns:
  - "**/test_*.py"
  - "**/__pycache__/**"
""")

        result = load_repo_filter_config(repo_path, base_dir=tmp_path)

        assert result.include_patterns == ["src/**/*.py", "lib/**/*.py"]
        assert result.exclude_patterns == ["**/test_*.py", "**/__pycache__/**"]

    def test_invalid_yaml_raises_config_error(self, tmp_path: Path):
        """Invalid YAML in config.yaml raises ConfigError."""
        from unfamiliar.config import (
            ConfigError,
            get_repo_config_path,
            load_repo_filter_config,
        )

        repo_path = tmp_path / "myproject"
        repo_path.mkdir()

        config_dir = get_repo_config_path(repo_path, base_dir=tmp_path)
        config_dir.mkdir(parents=True)
        (config_dir / "config.yaml").write_text("not: valid: yaml: [[[[")

        with pytest.raises(ConfigError) as exc_info:
            load_repo_filter_config(repo_path, base_dir=tmp_path)

        assert "Failed to parse" in str(exc_info.value)

    def test_config_with_only_include_patterns_sets_empty_exclude(self, tmp_path: Path):
        """Config with only include_patterns sets exclude_patterns to empty."""
        from unfamiliar.config import (
            get_repo_config_path,
            load_repo_filter_config,
        )

        repo_path = tmp_path / "myproject"
        repo_path.mkdir()

        config_dir = get_repo_config_path(repo_path, base_dir=tmp_path)
        config_dir.mkdir(parents=True)
        (config_dir / "config.yaml").write_text("""
include_patterns:
  - "src/**/*.py"
""")

        result = load_repo_filter_config(repo_path, base_dir=tmp_path)

        assert result.include_patterns == ["src/**/*.py"]
        assert result.exclude_patterns == []

    def test_config_with_only_exclude_patterns_sets_empty_include(self, tmp_path: Path):
        """Config with only exclude_patterns sets include_patterns to empty."""
        from unfamiliar.config import (
            get_repo_config_path,
            load_repo_filter_config,
        )

        repo_path = tmp_path / "myproject"
        repo_path.mkdir()

        config_dir = get_repo_config_path(repo_path, base_dir=tmp_path)
        config_dir.mkdir(parents=True)
        (config_dir / "config.yaml").write_text("""
exclude_patterns:
  - "**/node_modules/**"
""")

        result = load_repo_filter_config(repo_path, base_dir=tmp_path)

        assert result.include_patterns == []
        assert result.exclude_patterns == ["**/node_modules/**"]

    def test_config_with_null_values_raises_validation_error(self, tmp_path: Path):
        """Config with null values for patterns raises Pydantic validation error.

        Note: The implementation passes None directly to RepoFilterConfig which
        fails Pydantic validation. This is current behavior - null values are
        not gracefully handled.
        """
        from pydantic import ValidationError

        from unfamiliar.config import (
            get_repo_config_path,
            load_repo_filter_config,
        )

        repo_path = tmp_path / "myproject"
        repo_path.mkdir()

        config_dir = get_repo_config_path(repo_path, base_dir=tmp_path)
        config_dir.mkdir(parents=True)
        (config_dir / "config.yaml").write_text(
            "include_patterns: null\nexclude_patterns: null\n"
        )

        # Current implementation raises ValidationError when patterns are null
        with pytest.raises(ValidationError):
            load_repo_filter_config(repo_path, base_dir=tmp_path)
