"""Unit tests for ConfigManager."""

from pathlib import Path
from unittest.mock import patch
from uuid import uuid4

import pytest

from teams_phone.exceptions import ConfigurationError, NotFoundError, ValidationError
from teams_phone.infrastructure import ConfigManager
from teams_phone.models import (
    AuthMethod,
    CacheSettings,
    Config,
    NetworkSettings,
    TenantProfile,
)


class TestConfigManagerInit:
    """Tests for ConfigManager initialization."""

    @pytest.mark.unit
    def test_default_path(self) -> None:
        """ConfigManager uses default path when not specified."""
        manager = ConfigManager()
        assert manager.config_path == Path.home() / ".teams-phone" / "config.toml"

    @pytest.mark.unit
    def test_custom_path(self, tmp_path: Path) -> None:
        """ConfigManager accepts custom config path."""
        custom_path = tmp_path / "custom" / "config.toml"
        manager = ConfigManager(config_path=str(custom_path))
        assert manager.config_path == custom_path

    @pytest.mark.unit
    def test_expands_tilde(self) -> None:
        """ConfigManager expands ~ in path."""
        manager = ConfigManager(config_path="~/my-config.toml")
        assert manager.config_path == Path.home() / "my-config.toml"


class TestEnsureConfigDir:
    """Tests for ensure_config_dir method."""

    @pytest.mark.unit
    def test_creates_parent_directories(self, tmp_path: Path) -> None:
        """ensure_config_dir creates parent directories."""
        config_path = tmp_path / "deep" / "nested" / "config.toml"
        manager = ConfigManager(config_path=str(config_path))

        assert not config_path.parent.exists()
        manager.ensure_config_dir()
        assert config_path.parent.exists()

    @pytest.mark.unit
    def test_idempotent(self, tmp_path: Path) -> None:
        """ensure_config_dir is idempotent."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))

        manager.ensure_config_dir()
        manager.ensure_config_dir()  # Should not raise
        assert config_path.parent.exists()

    @pytest.mark.unit
    def test_permission_error_raises_configuration_error(self, tmp_path: Path) -> None:
        """ensure_config_dir raises ConfigurationError on permission failure."""
        config_path = tmp_path / "restricted" / "config.toml"
        manager = ConfigManager(config_path=str(config_path))

        with patch.object(Path, "mkdir", side_effect=PermissionError("Access denied")):
            with pytest.raises(ConfigurationError) as exc_info:
                manager.ensure_config_dir()

        assert "Cannot create configuration directory" in str(exc_info.value)
        assert exc_info.value.remediation is not None
        assert "permission" in exc_info.value.remediation.lower()


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

    @pytest.mark.unit
    def test_missing_file_returns_default(self, tmp_path: Path) -> None:
        """load_config returns default Config when file doesn't exist."""
        config_path = tmp_path / "nonexistent.toml"
        manager = ConfigManager(config_path=str(config_path))

        config = manager.load_config()

        assert isinstance(config, Config)
        assert config.default_tenant is None
        assert config.tenants == {}

    @pytest.mark.unit
    def test_loads_valid_toml(self, tmp_path: Path) -> None:
        """load_config parses valid TOML and returns Config."""
        config_path = tmp_path / "config.toml"
        config_path.write_text(
            """
default_tenant = "contoso"

[cache]
stale_days = 7

[network]
timeout = 60
""",
            encoding="utf-8",
        )
        manager = ConfigManager(config_path=str(config_path))

        config = manager.load_config()

        assert config.default_tenant == "contoso"
        assert config.cache.stale_days == 7
        assert config.network.timeout == 60

    @pytest.mark.unit
    def test_loads_config_with_tenants(self, tmp_path: Path) -> None:
        """load_config correctly parses nested tenant profiles."""
        tenant_id = str(uuid4())
        client_id = str(uuid4())
        config_path = tmp_path / "config.toml"
        config_path.write_text(
            f"""
default_tenant = "contoso"

[tenants.contoso]
name = "contoso"
tenant_id = "{tenant_id}"
client_id = "{client_id}"
display_name = "Contoso Ltd"
auth_method = "certificate"
cert_path = "/path/to/cert.pem"
""",
            encoding="utf-8",
        )
        manager = ConfigManager(config_path=str(config_path))

        config = manager.load_config()

        assert "contoso" in config.tenants
        tenant = config.tenants["contoso"]
        assert tenant.name == "contoso"
        assert str(tenant.tenant_id) == tenant_id
        assert tenant.auth_method == AuthMethod.CERTIFICATE
        assert tenant.cert_path == "/path/to/cert.pem"

    @pytest.mark.unit
    def test_invalid_toml_raises_configuration_error(self, tmp_path: Path) -> None:
        """load_config raises ConfigurationError for invalid TOML syntax."""
        config_path = tmp_path / "config.toml"
        config_path.write_text(
            """
invalid toml content [
missing =
""",
            encoding="utf-8",
        )
        manager = ConfigManager(config_path=str(config_path))

        with pytest.raises(ConfigurationError) as exc_info:
            manager.load_config()

        assert "Invalid TOML syntax" in str(exc_info.value)
        assert exc_info.value.remediation is not None
        assert "syntax errors" in exc_info.value.remediation

    @pytest.mark.unit
    def test_validation_error_raises_configuration_error(self, tmp_path: Path) -> None:
        """load_config raises ConfigurationError for invalid config values."""
        config_path = tmp_path / "config.toml"
        config_path.write_text(
            """
[cache]
stale_days = "not a number"
""",
            encoding="utf-8",
        )
        manager = ConfigManager(config_path=str(config_path))

        with pytest.raises(ConfigurationError) as exc_info:
            manager.load_config()

        assert "Invalid configuration" in str(exc_info.value)
        assert exc_info.value.remediation is not None

    @pytest.mark.unit
    def test_extra_field_raises_configuration_error(self, tmp_path: Path) -> None:
        """load_config raises ConfigurationError for unknown fields."""
        config_path = tmp_path / "config.toml"
        config_path.write_text(
            """
unknown_field = "should fail"
""",
            encoding="utf-8",
        )
        manager = ConfigManager(config_path=str(config_path))

        with pytest.raises(ConfigurationError) as exc_info:
            manager.load_config()

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


class TestSaveConfig:
    """Tests for save_config method."""

    @pytest.mark.unit
    def test_saves_default_config(self, tmp_path: Path) -> None:
        """save_config writes default Config to file."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))
        config = Config()

        manager.save_config(config)

        assert config_path.exists()
        content = config_path.read_text(encoding="utf-8")
        assert "config_path" in content

    @pytest.mark.unit
    def test_creates_parent_directories(self, tmp_path: Path) -> None:
        """save_config creates parent directories if needed."""
        config_path = tmp_path / "deep" / "nested" / "config.toml"
        manager = ConfigManager(config_path=str(config_path))
        config = Config()

        manager.save_config(config)

        assert config_path.exists()

    @pytest.mark.unit
    def test_saves_config_with_tenants(self, tmp_path: Path) -> None:
        """save_config correctly serializes nested tenant profiles."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))

        tenant_id = uuid4()
        client_id = uuid4()
        profile = TenantProfile(
            name="contoso",
            tenant_id=tenant_id,
            client_id=client_id,
            display_name="Contoso Ltd",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path="/path/to/cert.pem",
        )
        config = Config(
            default_tenant="contoso",
            tenants={"contoso": profile},
        )

        manager.save_config(config)

        content = config_path.read_text(encoding="utf-8")
        assert str(tenant_id) in content
        assert "certificate" in content
        assert "Contoso Ltd" in content

    @pytest.mark.unit
    def test_round_trip(self, tmp_path: Path) -> None:
        """Config survives save then load round-trip."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))

        tenant_id = uuid4()
        client_id = uuid4()
        profile = TenantProfile(
            name="test",
            tenant_id=tenant_id,
            client_id=client_id,
            display_name="Test Corp",
            auth_method=AuthMethod.CLIENT_SECRET,
        )
        original = Config(
            default_tenant="test",
            cache=CacheSettings(cache_path="/custom/cache", stale_days=14),
            network=NetworkSettings(timeout=45, max_retries=5),
            tenants={"test": profile},
        )

        manager.save_config(original)
        loaded = manager.load_config()

        assert loaded.default_tenant == original.default_tenant
        assert loaded.cache.stale_days == 14
        assert loaded.network.timeout == 45
        assert "test" in loaded.tenants
        assert loaded.tenants["test"].tenant_id == tenant_id
        assert loaded.tenants["test"].auth_method == AuthMethod.CLIENT_SECRET

    @pytest.mark.unit
    def test_overwrites_existing_file(self, tmp_path: Path) -> None:
        """save_config overwrites existing file."""
        config_path = tmp_path / "config.toml"
        config_path.write_text("old content", encoding="utf-8")
        manager = ConfigManager(config_path=str(config_path))

        manager.save_config(Config(default_tenant="new"))

        content = config_path.read_text(encoding="utf-8")
        assert "old content" not in content
        assert "new" in content


class TestGetTenant:
    """Tests for get_tenant method."""

    @pytest.mark.unit
    def test_returns_existing_tenant(self, tmp_path: Path) -> None:
        """get_tenant returns the tenant profile when it exists."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))

        tenant_id = uuid4()
        client_id = uuid4()
        profile = TenantProfile(
            name="contoso",
            tenant_id=tenant_id,
            client_id=client_id,
            display_name="Contoso Ltd",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path="/path/to/cert.pem",
        )
        config = Config(tenants={"contoso": profile})
        manager.save_config(config)

        result = manager.get_tenant("contoso")

        assert result.name == "contoso"
        assert result.tenant_id == tenant_id
        assert result.display_name == "Contoso Ltd"

    @pytest.mark.unit
    def test_raises_not_found_for_missing_tenant(self, tmp_path: Path) -> None:
        """get_tenant raises NotFoundError when tenant doesn't exist."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))
        manager.save_config(Config())

        with pytest.raises(NotFoundError) as exc_info:
            manager.get_tenant("nonexistent")

        assert "Tenant 'nonexistent' not found" in str(exc_info.value)
        assert exc_info.value.remediation is not None
        assert "tenants list" in exc_info.value.remediation

    @pytest.mark.unit
    def test_raises_not_found_when_no_config_file(self, tmp_path: Path) -> None:
        """get_tenant raises NotFoundError when config file doesn't exist."""
        config_path = tmp_path / "nonexistent.toml"
        manager = ConfigManager(config_path=str(config_path))

        with pytest.raises(NotFoundError) as exc_info:
            manager.get_tenant("any")

        assert "Tenant 'any' not found" in str(exc_info.value)


class TestSetTenant:
    """Tests for set_tenant method."""

    @pytest.mark.unit
    def test_adds_new_tenant(self, tmp_path: Path) -> None:
        """set_tenant adds a new tenant to empty config."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))

        tenant_id = uuid4()
        client_id = uuid4()
        profile = TenantProfile(
            name="contoso",
            tenant_id=tenant_id,
            client_id=client_id,
            display_name="Contoso Ltd",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path="/path/to/cert.pem",
        )

        manager.set_tenant("contoso", profile)

        config = manager.load_config()
        assert "contoso" in config.tenants
        assert config.tenants["contoso"].tenant_id == tenant_id

    @pytest.mark.unit
    def test_updates_existing_tenant(self, tmp_path: Path) -> None:
        """set_tenant updates an existing tenant profile."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))

        tenant_id = uuid4()
        client_id = uuid4()
        original = TenantProfile(
            name="contoso",
            tenant_id=tenant_id,
            client_id=client_id,
            display_name="Original Name",
            auth_method=AuthMethod.CERTIFICATE,
        )
        manager.set_tenant("contoso", original)

        updated = TenantProfile(
            name="contoso",
            tenant_id=tenant_id,
            client_id=client_id,
            display_name="Updated Name",
            auth_method=AuthMethod.CLIENT_SECRET,
        )
        manager.set_tenant("contoso", updated)

        config = manager.load_config()
        assert config.tenants["contoso"].display_name == "Updated Name"
        assert config.tenants["contoso"].auth_method == AuthMethod.CLIENT_SECRET

    @pytest.mark.unit
    def test_raises_validation_error_for_name_mismatch(self, tmp_path: Path) -> None:
        """set_tenant raises ValidationError when profile.name doesn't match key."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))

        profile = TenantProfile(
            name="different",
            tenant_id=uuid4(),
            client_id=uuid4(),
            display_name="Test",
            auth_method=AuthMethod.CERTIFICATE,
        )

        with pytest.raises(ValidationError) as exc_info:
            manager.set_tenant("contoso", profile)

        assert "does not match key" in str(exc_info.value)
        assert exc_info.value.remediation is not None

    @pytest.mark.unit
    def test_preserves_other_tenants(self, tmp_path: Path) -> None:
        """set_tenant preserves existing tenants when adding a new one."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))

        tenant1 = TenantProfile(
            name="tenant1",
            tenant_id=uuid4(),
            client_id=uuid4(),
            display_name="Tenant 1",
            auth_method=AuthMethod.CERTIFICATE,
        )
        tenant2 = TenantProfile(
            name="tenant2",
            tenant_id=uuid4(),
            client_id=uuid4(),
            display_name="Tenant 2",
            auth_method=AuthMethod.CLIENT_SECRET,
        )

        manager.set_tenant("tenant1", tenant1)
        manager.set_tenant("tenant2", tenant2)

        config = manager.load_config()
        assert "tenant1" in config.tenants
        assert "tenant2" in config.tenants


class TestRemoveTenant:
    """Tests for remove_tenant method."""

    @pytest.mark.unit
    def test_removes_existing_tenant(self, tmp_path: Path) -> None:
        """remove_tenant removes a tenant from config."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))

        profile = TenantProfile(
            name="contoso",
            tenant_id=uuid4(),
            client_id=uuid4(),
            display_name="Contoso Ltd",
            auth_method=AuthMethod.CERTIFICATE,
        )
        config = Config(tenants={"contoso": profile})
        manager.save_config(config)

        manager.remove_tenant("contoso")

        config = manager.load_config()
        assert "contoso" not in config.tenants

    @pytest.mark.unit
    def test_raises_not_found_for_missing_tenant(self, tmp_path: Path) -> None:
        """remove_tenant raises NotFoundError when tenant doesn't exist."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))
        manager.save_config(Config())

        with pytest.raises(NotFoundError) as exc_info:
            manager.remove_tenant("nonexistent")

        assert "Tenant 'nonexistent' not found" in str(exc_info.value)

    @pytest.mark.unit
    def test_clears_default_tenant_if_removed(self, tmp_path: Path) -> None:
        """remove_tenant clears default_tenant if the removed tenant was default."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))

        profile = TenantProfile(
            name="contoso",
            tenant_id=uuid4(),
            client_id=uuid4(),
            display_name="Contoso Ltd",
            auth_method=AuthMethod.CERTIFICATE,
        )
        config = Config(
            default_tenant="contoso",
            tenants={"contoso": profile},
        )
        manager.save_config(config)

        manager.remove_tenant("contoso")

        config = manager.load_config()
        assert config.default_tenant is None

    @pytest.mark.unit
    def test_preserves_default_tenant_if_different(self, tmp_path: Path) -> None:
        """remove_tenant preserves default_tenant if it's a different tenant."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))

        profile1 = TenantProfile(
            name="contoso",
            tenant_id=uuid4(),
            client_id=uuid4(),
            display_name="Contoso Ltd",
            auth_method=AuthMethod.CERTIFICATE,
        )
        profile2 = TenantProfile(
            name="fabrikam",
            tenant_id=uuid4(),
            client_id=uuid4(),
            display_name="Fabrikam Inc",
            auth_method=AuthMethod.CLIENT_SECRET,
        )
        config = Config(
            default_tenant="fabrikam",
            tenants={"contoso": profile1, "fabrikam": profile2},
        )
        manager.save_config(config)

        manager.remove_tenant("contoso")

        config = manager.load_config()
        assert config.default_tenant == "fabrikam"
        assert "fabrikam" in config.tenants
        assert "contoso" not in config.tenants

    @pytest.mark.unit
    def test_preserves_other_tenants(self, tmp_path: Path) -> None:
        """remove_tenant preserves other tenants when removing one."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))

        profile1 = TenantProfile(
            name="contoso",
            tenant_id=uuid4(),
            client_id=uuid4(),
            display_name="Contoso Ltd",
            auth_method=AuthMethod.CERTIFICATE,
        )
        profile2 = TenantProfile(
            name="fabrikam",
            tenant_id=uuid4(),
            client_id=uuid4(),
            display_name="Fabrikam Inc",
            auth_method=AuthMethod.CLIENT_SECRET,
        )
        config = Config(tenants={"contoso": profile1, "fabrikam": profile2})
        manager.save_config(config)

        manager.remove_tenant("contoso")

        config = manager.load_config()
        assert "fabrikam" in config.tenants
        assert config.tenants["fabrikam"].display_name == "Fabrikam Inc"


class TestGetDefaultTenant:
    """Tests for get_default_tenant method."""

    @pytest.mark.unit
    def test_returns_none_for_empty_config(self, tmp_path: Path) -> None:
        """get_default_tenant returns None when config file doesn't exist."""
        config_path = tmp_path / "nonexistent.toml"
        manager = ConfigManager(config_path=str(config_path))

        result = manager.get_default_tenant()

        assert result is None

    @pytest.mark.unit
    def test_returns_configured_default(self, tmp_path: Path) -> None:
        """get_default_tenant returns the configured default tenant name."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))

        profile = TenantProfile(
            name="contoso",
            tenant_id=uuid4(),
            client_id=uuid4(),
            display_name="Contoso Ltd",
            auth_method=AuthMethod.CERTIFICATE,
        )
        config = Config(
            default_tenant="contoso",
            tenants={"contoso": profile},
        )
        manager.save_config(config)

        result = manager.get_default_tenant()

        assert result == "contoso"


class TestSetDefaultTenant:
    """Tests for set_default_tenant method."""

    @pytest.mark.unit
    def test_updates_default_value(self, tmp_path: Path) -> None:
        """set_default_tenant sets the default tenant."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))

        profile = TenantProfile(
            name="contoso",
            tenant_id=uuid4(),
            client_id=uuid4(),
            display_name="Contoso Ltd",
            auth_method=AuthMethod.CERTIFICATE,
        )
        config = Config(tenants={"contoso": profile})
        manager.save_config(config)

        manager.set_default_tenant("contoso")

        config = manager.load_config()
        assert config.default_tenant == "contoso"

    @pytest.mark.unit
    def test_raises_not_found_for_nonexistent_tenant(self, tmp_path: Path) -> None:
        """set_default_tenant raises NotFoundError for nonexistent tenant."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))
        manager.save_config(Config())

        with pytest.raises(NotFoundError) as exc_info:
            manager.set_default_tenant("nonexistent")

        assert "Tenant 'nonexistent' not found" in str(exc_info.value)
        assert exc_info.value.remediation is not None
        assert "tenants list" in exc_info.value.remediation

    @pytest.mark.unit
    def test_changes_existing_default(self, tmp_path: Path) -> None:
        """set_default_tenant changes an already-set default tenant."""
        config_path = tmp_path / "config.toml"
        manager = ConfigManager(config_path=str(config_path))

        profile1 = TenantProfile(
            name="contoso",
            tenant_id=uuid4(),
            client_id=uuid4(),
            display_name="Contoso Ltd",
            auth_method=AuthMethod.CERTIFICATE,
        )
        profile2 = TenantProfile(
            name="fabrikam",
            tenant_id=uuid4(),
            client_id=uuid4(),
            display_name="Fabrikam Inc",
            auth_method=AuthMethod.CLIENT_SECRET,
        )
        config = Config(
            default_tenant="contoso",
            tenants={"contoso": profile1, "fabrikam": profile2},
        )
        manager.save_config(config)

        manager.set_default_tenant("fabrikam")

        config = manager.load_config()
        assert config.default_tenant == "fabrikam"
