"""Tests for privacy networking module."""

from __future__ import annotations

from datetime import datetime
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from gaol.privacy import (
    DNSLeakConfig,
    FirewallBackend,
    HiddenServiceConfig,
    HiddenServiceState,
    PrivacyFirewall,
    PrivacyMode,
    PrivacyStatus,
    TorProxyConfig,
    TorProxyState,
    VPNGatewayConfig,
    VPNGatewayState,
    VPNProvider,
    WireGuardInterface,
    WireGuardMeshConfig,
    WireGuardMeshState,
    WireGuardPeer,
)
from gaol.privacy.exceptions import (
    FirewallNotAvailableError,
    HiddenServiceAlreadyExistsError,
    HiddenServiceNotFoundError,
    PrivacyError,
    TorBootstrapError,
    TorError,
    TorNotFoundError,
    TorProxyAlreadyExistsError,
    TorProxyNotFoundError,
    VPNConfigError,
    VPNConnectionError,
    VPNGatewayAlreadyExistsError,
    VPNGatewayNotFoundError,
    WireGuardMeshAlreadyExistsError,
    WireGuardMeshNotFoundError,
    WireGuardNotFoundError,
    WireGuardPeerError,
)


class TestPrivacyModels:
    """Tests for privacy configuration models."""

    def test_privacy_mode_enum(self) -> None:
        """Test PrivacyMode enum values."""
        assert PrivacyMode.NONE.value == "none"
        assert PrivacyMode.TOR_PROXY.value == "tor_proxy"
        assert PrivacyMode.TOR_TRANSPARENT.value == "tor_transparent"
        assert PrivacyMode.VPN.value == "vpn"
        assert PrivacyMode.WIREGUARD.value == "wireguard"

    def test_privacy_status_enum(self) -> None:
        """Test PrivacyStatus enum values."""
        assert PrivacyStatus.RUNNING.value == "running"
        assert PrivacyStatus.STOPPED.value == "stopped"
        assert PrivacyStatus.STARTING.value == "starting"
        assert PrivacyStatus.ERROR.value == "error"
        assert PrivacyStatus.BOOTSTRAPPING.value == "bootstrapping"


class TestTorModels:
    """Tests for Tor configuration models."""

    def test_tor_proxy_config_defaults(self) -> None:
        """Test TorProxyConfig default values."""
        config = TorProxyConfig()
        assert config.socks_port == 9050
        assert config.control_port == 9051
        assert config.dns_port == 5353
        assert config.trans_port == 9040
        assert config.transparent is False
        assert config.exit_nodes == []
        assert config.exclude_nodes == []
        assert config.bridge_enabled is False
        assert config.bridges == []

    def test_tor_proxy_config_custom(self) -> None:
        """Test TorProxyConfig with custom values."""
        config = TorProxyConfig(
            socks_port=9150,
            control_port=9151,
            transparent=True,
            exit_nodes=["US", "DE"],
            strict_nodes=True,
        )
        assert config.socks_port == 9150
        assert config.control_port == 9151
        assert config.transparent is True
        assert config.exit_nodes == ["US", "DE"]
        assert config.strict_nodes is True

    def test_tor_proxy_state_creation(self) -> None:
        """Test creating TorProxyState model."""
        state = TorProxyState(
            name="mytor",
            container_name="mycontainer",
            pid=12345,
            socks_port=9050,
            control_port=9051,
            dns_port=5353,
            data_dir=Path("/tmp/tor"),
            status=PrivacyStatus.RUNNING,
            bootstrap_progress=100,
        )
        assert state.name == "mytor"
        assert state.container_name == "mycontainer"
        assert state.pid == 12345
        assert state.socks_port == 9050
        assert state.status == PrivacyStatus.RUNNING
        assert state.bootstrap_progress == 100

    def test_hidden_service_config(self) -> None:
        """Test HiddenServiceConfig model."""
        config = HiddenServiceConfig(
            name="webservice",
            container_name="nginx",
            container_port=80,
            hidden_service_port=80,
        )
        assert config.name == "webservice"
        assert config.container_name == "nginx"
        assert config.container_port == 80
        assert config.hidden_service_port == 80
        assert config.version == 3  # Default v3

    def test_hidden_service_state(self) -> None:
        """Test HiddenServiceState model."""
        state = HiddenServiceState(
            name="webservice",
            onion_address="abc123.onion",
            container_name="nginx",
            container_port=80,
            hidden_service_port=80,
            data_dir=Path("/tmp/hs"),
            status=PrivacyStatus.RUNNING,
        )
        assert state.name == "webservice"
        assert state.onion_address == "abc123.onion"
        assert state.status == PrivacyStatus.RUNNING


class TestVPNModels:
    """Tests for VPN configuration models."""

    def test_vpn_provider_enum(self) -> None:
        """Test VPNProvider enum values."""
        assert VPNProvider.OPENVPN.value == "openvpn"
        assert VPNProvider.WIREGUARD.value == "wireguard"

    def test_vpn_gateway_config(self) -> None:
        """Test VPNGatewayConfig model."""
        config = VPNGatewayConfig(
            name="myvpn",
            provider=VPNProvider.OPENVPN,
            config_file=Path("/home/user/vpn.ovpn"),
            kill_switch=True,
        )
        assert config.name == "myvpn"
        assert config.provider == VPNProvider.OPENVPN
        assert config.kill_switch is True
        assert config.dns_servers == ["1.1.1.1", "1.0.0.1"]  # Default

    def test_vpn_gateway_state(self) -> None:
        """Test VPNGatewayState model."""
        state = VPNGatewayState(
            name="myvpn",
            provider=VPNProvider.OPENVPN,
            pid=54321,
            interface="tun0",
            public_ip="1.2.3.4",
            status=PrivacyStatus.RUNNING,
        )
        assert state.name == "myvpn"
        assert state.provider == VPNProvider.OPENVPN
        assert state.interface == "tun0"
        assert state.public_ip == "1.2.3.4"


class TestWireGuardModels:
    """Tests for WireGuard configuration models."""

    def test_wireguard_peer(self) -> None:
        """Test WireGuardPeer model."""
        peer = WireGuardPeer(
            name="peer1",
            public_key="ABCD1234==",
            endpoint="10.0.0.2:51820",
            allowed_ips=["10.200.0.2/32"],
            persistent_keepalive=25,
        )
        assert peer.name == "peer1"
        assert peer.public_key == "ABCD1234=="
        assert peer.endpoint == "10.0.0.2:51820"
        assert peer.allowed_ips == ["10.200.0.2/32"]
        assert peer.persistent_keepalive == 25

    def test_wireguard_interface(self) -> None:
        """Test WireGuardInterface model."""
        interface = WireGuardInterface(
            private_key="PRIVATEKEY==",
            address="10.200.0.1/24",
            listen_port=51820,
        )
        assert interface.private_key == "PRIVATEKEY=="
        assert interface.address == "10.200.0.1/24"
        assert interface.listen_port == 51820
        assert interface.mtu == 1420  # Default

    def test_wireguard_mesh_config(self) -> None:
        """Test WireGuardMeshConfig model."""
        config = WireGuardMeshConfig(
            name="mymesh",
            subnet="10.200.0.0/24",
            interface_name="wg0",
            listen_port=51820,
        )
        assert config.name == "mymesh"
        assert config.subnet == "10.200.0.0/24"
        assert config.interface_name == "wg0"
        assert config.peers == []

    def test_wireguard_mesh_state(self) -> None:
        """Test WireGuardMeshState model."""
        interface = WireGuardInterface(
            private_key="KEY==",
            address="10.200.0.1/24",
            listen_port=51820,
        )
        state = WireGuardMeshState(
            name="mymesh",
            interface=interface,
            peers=[],
            interface_name="wg-mymesh",
            status=PrivacyStatus.STOPPED,
        )
        assert state.name == "mymesh"
        assert state.interface is not None
        assert state.interface_name == "wg-mymesh"
        assert state.status == PrivacyStatus.STOPPED


class TestDNSLeakConfig:
    """Tests for DNS leak prevention configuration."""

    def test_dns_leak_config_defaults(self) -> None:
        """Test DNSLeakConfig default values."""
        config = DNSLeakConfig()
        assert config.enabled is True
        assert config.dns_servers == []
        assert config.block_direct_dns is True
        assert config.allow_local is False

    def test_dns_leak_config_custom(self) -> None:
        """Test DNSLeakConfig with custom values."""
        config = DNSLeakConfig(
            enabled=True,
            dns_servers=["1.1.1.1", "8.8.8.8"],
            block_direct_dns=True,
            allow_local=True,
        )
        assert config.dns_servers == ["1.1.1.1", "8.8.8.8"]
        assert config.allow_local is True


class TestPrivacyExceptions:
    """Tests for privacy exceptions."""

    def test_privacy_error_base(self) -> None:
        """Test base PrivacyError."""
        error = PrivacyError("Test error")
        assert str(error) == "Test error"

    def test_tor_not_found_error(self) -> None:
        """Test TorNotFoundError."""
        error = TorNotFoundError()
        assert "tor not found" in str(error).lower()
        assert "apt install" in str(error).lower()

    def test_tor_proxy_already_exists_error(self) -> None:
        """Test TorProxyAlreadyExistsError."""
        error = TorProxyAlreadyExistsError("myproxy")
        assert "myproxy" in str(error)

    def test_tor_proxy_not_found_error(self) -> None:
        """Test TorProxyNotFoundError."""
        error = TorProxyNotFoundError("myproxy")
        assert error.name == "myproxy"
        assert "myproxy" in str(error)

    def test_tor_bootstrap_error(self) -> None:
        """Test TorBootstrapError."""
        error = TorBootstrapError(50, "Connection timeout")
        assert error.progress == 50
        assert "50%" in str(error)
        assert "Connection timeout" in str(error)

    def test_hidden_service_errors(self) -> None:
        """Test hidden service exceptions."""
        error1 = HiddenServiceAlreadyExistsError("web")
        assert "web" in str(error1)

        error2 = HiddenServiceNotFoundError("web")
        assert error2.name == "web"

    def test_vpn_errors(self) -> None:
        """Test VPN exceptions."""
        error1 = VPNGatewayAlreadyExistsError("vpn1")
        assert "vpn1" in str(error1)

        error2 = VPNGatewayNotFoundError("vpn1")
        assert error2.name == "vpn1"

        error3 = VPNConnectionError("Connection refused")
        assert "Connection refused" in str(error3)

        error4 = VPNConfigError("Invalid config")
        assert "Invalid config" in str(error4)

    def test_wireguard_errors(self) -> None:
        """Test WireGuard exceptions."""
        error1 = WireGuardNotFoundError()
        assert "wireguard-tools" in str(error1).lower()

        error2 = WireGuardMeshAlreadyExistsError("mesh1")
        assert "mesh1" in str(error2)

        error3 = WireGuardMeshNotFoundError("mesh1")
        assert error3.name == "mesh1"

        error4 = WireGuardPeerError("peer1", "Invalid key")
        assert "peer1" in str(error4)
        assert "Invalid key" in str(error4)

    def test_firewall_not_available_error(self) -> None:
        """Test FirewallNotAvailableError."""
        error = FirewallNotAvailableError()
        assert "iptables" in str(error).lower() or "nftables" in str(error).lower()


class TestPrivacyFirewall:
    """Tests for PrivacyFirewall class."""

    @patch("gaol.privacy.firewall.shutil.which")
    def test_firewall_backend_nftables(self, mock_which: MagicMock) -> None:
        """Test firewall backend detection for nftables."""
        mock_which.return_value = "/usr/sbin/nft"
        with patch("gaol.privacy.firewall._run_cmd") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)
            firewall = PrivacyFirewall()
            assert firewall.backend == FirewallBackend.NFTABLES

    @patch("gaol.privacy.firewall.shutil.which")
    def test_firewall_backend_iptables(self, mock_which: MagicMock) -> None:
        """Test firewall backend detection for iptables."""
        def which_side_effect(cmd: str) -> str | None:
            if cmd == "nft":
                return None
            if cmd == "iptables":
                return "/usr/sbin/iptables"
            return None

        mock_which.side_effect = which_side_effect
        firewall = PrivacyFirewall()
        assert firewall.backend == FirewallBackend.IPTABLES

    @patch("gaol.privacy.firewall.shutil.which")
    def test_firewall_not_available(self, mock_which: MagicMock) -> None:
        """Test firewall not available raises error."""
        mock_which.return_value = None
        firewall = PrivacyFirewall()
        with pytest.raises(FirewallNotAvailableError):
            _ = firewall.backend

    @patch("gaol.privacy.firewall.shutil.which")
    def test_firewall_is_available(self, mock_which: MagicMock) -> None:
        """Test is_available method."""
        mock_which.return_value = "/usr/sbin/iptables"
        firewall = PrivacyFirewall()
        firewall._backend = FirewallBackend.IPTABLES  # Skip detection
        assert firewall.is_available() is True


class TestTorBackend:
    """Tests for TorBackend class."""

    @patch("gaol.privacy.tor.backend.shutil.which")
    def test_tor_backend_available(self, mock_which: MagicMock) -> None:
        """Test TorBackend is_available."""
        mock_which.return_value = "/usr/bin/tor"
        from gaol.privacy.tor import TorBackend

        backend = TorBackend()
        assert backend.is_available() is True

    @patch("gaol.privacy.tor.backend.shutil.which")
    def test_tor_backend_not_available(self, mock_which: MagicMock) -> None:
        """Test TorBackend not available."""
        mock_which.return_value = None
        from gaol.privacy.tor import TorBackend

        backend = TorBackend()
        assert backend.is_available() is False

    @patch("gaol.privacy.tor.backend.shutil.which")
    def test_tor_backend_binary_not_found(self, mock_which: MagicMock) -> None:
        """Test TorBackend raises error when tor not found."""
        mock_which.return_value = None
        from gaol.privacy.tor import TorBackend

        backend = TorBackend()
        with pytest.raises(TorNotFoundError):
            _ = backend.binary


class TestWireGuardBackend:
    """Tests for WireGuardBackend class."""

    @patch("gaol.privacy.wireguard.backend.shutil.which")
    def test_wireguard_backend_available(self, mock_which: MagicMock) -> None:
        """Test WireGuardBackend is_available."""
        mock_which.return_value = "/usr/bin/wg"
        from gaol.privacy.wireguard import WireGuardBackend

        backend = WireGuardBackend()
        assert backend.is_available() is True

    @patch("gaol.privacy.wireguard.backend.shutil.which")
    def test_wireguard_backend_not_available(self, mock_which: MagicMock) -> None:
        """Test WireGuardBackend not available."""
        mock_which.return_value = None
        from gaol.privacy.wireguard import WireGuardBackend

        backend = WireGuardBackend()
        assert backend.is_available() is False

    @patch("gaol.privacy.wireguard.backend.shutil.which")
    @patch("gaol.privacy.wireguard.backend.subprocess.run")
    def test_wireguard_generate_keypair(
        self, mock_run: MagicMock, mock_which: MagicMock
    ) -> None:
        """Test WireGuard keypair generation."""
        mock_which.return_value = "/usr/bin/wg"

        # Mock wg genkey
        mock_genkey = MagicMock()
        mock_genkey.returncode = 0
        mock_genkey.stdout = "PRIVATE_KEY=="

        # Mock wg pubkey
        mock_pubkey = MagicMock()
        mock_pubkey.returncode = 0
        mock_pubkey.stdout = "PUBLIC_KEY=="

        mock_run.side_effect = [mock_genkey, mock_pubkey]

        from gaol.privacy.wireguard import WireGuardBackend

        backend = WireGuardBackend()
        private_key, public_key = backend.generate_keypair()

        assert private_key == "PRIVATE_KEY=="
        assert public_key == "PUBLIC_KEY=="


class TestTorProxyManager:
    """Tests for TorProxyManager class."""

    def test_manager_initialization(self, tmp_path: Path) -> None:
        """Test TorProxyManager initialization."""
        from gaol.privacy.tor import TorProxyManager

        manager = TorProxyManager(state_dir=tmp_path)
        assert manager.state_dir == tmp_path
        assert manager._store_path == tmp_path / "proxies.yaml"

    def test_manager_list_empty(self, tmp_path: Path) -> None:
        """Test listing proxies when none exist."""
        from gaol.privacy.tor import TorProxyManager

        manager = TorProxyManager(state_dir=tmp_path)
        proxies = manager.list()
        assert proxies == []

    def test_manager_get_not_found(self, tmp_path: Path) -> None:
        """Test getting a proxy that doesn't exist."""
        from gaol.privacy.tor import TorProxyManager

        manager = TorProxyManager(state_dir=tmp_path)
        proxy = manager.get("nonexistent")
        assert proxy is None


class TestHiddenServiceManager:
    """Tests for HiddenServiceManager class."""

    def test_manager_initialization(self, tmp_path: Path) -> None:
        """Test HiddenServiceManager initialization."""
        from gaol.privacy.tor import HiddenServiceManager

        manager = HiddenServiceManager(state_dir=tmp_path)
        assert manager.state_dir == tmp_path
        assert manager._store_path == tmp_path / "hidden_services.yaml"

    def test_manager_list_empty(self, tmp_path: Path) -> None:
        """Test listing services when none exist."""
        from gaol.privacy.tor import HiddenServiceManager

        manager = HiddenServiceManager(state_dir=tmp_path)
        services = manager.list()
        assert services == []


class TestVPNGatewayManager:
    """Tests for VPNGatewayManager class."""

    def test_manager_initialization(self, tmp_path: Path) -> None:
        """Test VPNGatewayManager initialization."""
        from gaol.privacy.vpn import VPNGatewayManager

        manager = VPNGatewayManager(state_dir=tmp_path)
        assert manager.state_dir == tmp_path
        assert manager._store_path == tmp_path / "gateways.yaml"

    def test_manager_list_empty(self, tmp_path: Path) -> None:
        """Test listing gateways when none exist."""
        from gaol.privacy.vpn import VPNGatewayManager

        manager = VPNGatewayManager(state_dir=tmp_path)
        gateways = manager.list()
        assert gateways == []

    def test_manager_get_not_found(self, tmp_path: Path) -> None:
        """Test getting a gateway that doesn't exist."""
        from gaol.privacy.vpn import VPNGatewayManager

        manager = VPNGatewayManager(state_dir=tmp_path)
        gateway = manager.get("nonexistent")
        assert gateway is None


class TestWireGuardMeshManager:
    """Tests for WireGuardMeshManager class."""

    def test_manager_initialization(self, tmp_path: Path) -> None:
        """Test WireGuardMeshManager initialization."""
        from gaol.privacy.wireguard import WireGuardMeshManager

        manager = WireGuardMeshManager(state_dir=tmp_path)
        assert manager.state_dir == tmp_path
        assert manager._store_path == tmp_path / "meshes.yaml"

    def test_manager_list_empty(self, tmp_path: Path) -> None:
        """Test listing meshes when none exist."""
        from gaol.privacy.wireguard import WireGuardMeshManager

        manager = WireGuardMeshManager(state_dir=tmp_path)
        meshes = manager.list()
        assert meshes == []

    def test_manager_get_not_found(self, tmp_path: Path) -> None:
        """Test getting a mesh that doesn't exist."""
        from gaol.privacy.wireguard import WireGuardMeshManager

        manager = WireGuardMeshManager(state_dir=tmp_path)
        mesh = manager.get("nonexistent")
        assert mesh is None

    @patch("gaol.privacy.wireguard.manager.WireGuardBackend")
    def test_manager_create_mesh(self, mock_backend_class: MagicMock, tmp_path: Path) -> None:
        """Test creating a WireGuard mesh."""
        # Create the expected directory structure
        mesh_dir = tmp_path / "mymesh"
        mesh_dir.mkdir(parents=True, exist_ok=True)

        mock_backend = MagicMock()
        mock_backend.generate_keypair.return_value = ("PRIVATE==", "PUBLIC==")
        mock_backend.create_interface.return_value = mesh_dir / "mymesh.conf"
        mock_backend.is_interface_up.return_value = False
        mock_backend_class.return_value = mock_backend

        from gaol.privacy.wireguard import WireGuardMeshManager

        manager = WireGuardMeshManager(state_dir=tmp_path)
        mesh = manager.create("mymesh", subnet="10.200.0.0/24")

        assert mesh.name == "mymesh"
        assert mesh.interface is not None
        assert mesh.interface.private_key == "PRIVATE=="
        assert mesh.interface_name == "wg-mymesh"
