"""Tests for dev tunnels module."""

from __future__ import annotations

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

import pytest
from typer.testing import CliRunner

from gaol.tunnel import (
    ActiveTunnel,
    CloudflaredBackend,
    CloudflaredNotFoundError,
    TunnelAlreadyExistsError,
    TunnelConfig,
    TunnelError,
    TunnelManager,
    TunnelNotFoundError,
    TunnelStartError,
    TunnelStatus,
)


class TestTunnelModels:
    """Tests for tunnel Pydantic models."""

    def test_tunnel_status_values(self) -> None:
        """Test TunnelStatus enum values."""
        assert TunnelStatus.RUNNING.value == "running"
        assert TunnelStatus.STOPPED.value == "stopped"
        assert TunnelStatus.ERROR.value == "error"

    def test_tunnel_config_creation(self) -> None:
        """Test creating a tunnel config."""
        config = TunnelConfig(
            container_name="myapp",
            container_port=8080,
        )
        assert config.container_name == "myapp"
        assert config.container_port == 8080
        assert config.host_port is None
        assert config.protocol == "http"

    def test_tunnel_config_with_all_fields(self) -> None:
        """Test tunnel config with all optional fields."""
        config = TunnelConfig(
            container_name="myapp",
            container_port=8080,
            host_port=9000,
            protocol="https",
        )
        assert config.host_port == 9000
        assert config.protocol == "https"

    def test_active_tunnel_creation(self) -> None:
        """Test creating an active tunnel record."""
        tunnel = ActiveTunnel(
            id="abc123",
            container_name="myapp",
            container_port=8080,
            host_port=8080,
            public_url="https://test.trycloudflare.com",
            process_pid=12345,
        )
        assert tunnel.id == "abc123"
        assert tunnel.container_name == "myapp"
        assert tunnel.container_port == 8080
        assert tunnel.host_port == 8080
        assert tunnel.public_url == "https://test.trycloudflare.com"
        assert tunnel.process_pid == 12345
        assert tunnel.status == TunnelStatus.RUNNING
        assert isinstance(tunnel.started_at, datetime)

    def test_active_tunnel_with_custom_status(self) -> None:
        """Test active tunnel with custom status."""
        tunnel = ActiveTunnel(
            id="xyz789",
            container_name="web",
            container_port=3000,
            host_port=3001,
            public_url="https://random.trycloudflare.com",
            process_pid=54321,
            status=TunnelStatus.STOPPED,
        )
        assert tunnel.status == TunnelStatus.STOPPED


class TestTunnelExceptions:
    """Tests for tunnel exceptions."""

    def test_tunnel_error_is_base(self) -> None:
        """Test TunnelError is the base exception."""
        error = TunnelError("test error")
        assert str(error) == "test error"
        assert isinstance(error, Exception)

    def test_cloudflared_not_found_error(self) -> None:
        """Test CloudflaredNotFoundError."""
        error = CloudflaredNotFoundError()
        assert "cloudflared not found" in str(error)
        assert "brew install cloudflared" in str(error)
        assert isinstance(error, TunnelError)

    def test_tunnel_already_exists_error(self) -> None:
        """Test TunnelAlreadyExistsError."""
        error = TunnelAlreadyExistsError("myapp", 8080)
        assert error.container == "myapp"
        assert error.port == 8080
        assert "myapp:8080" in str(error)
        assert isinstance(error, TunnelError)

    def test_tunnel_not_found_error(self) -> None:
        """Test TunnelNotFoundError."""
        error = TunnelNotFoundError("abc123")
        assert error.tunnel_id == "abc123"
        assert "abc123" in str(error)
        assert isinstance(error, TunnelError)

    def test_tunnel_start_error(self) -> None:
        """Test TunnelStartError."""
        error = TunnelStartError("connection refused")
        assert error.reason == "connection refused"
        assert "connection refused" in str(error)
        assert isinstance(error, TunnelError)


class TestCloudflaredBackend:
    """Tests for cloudflared backend."""

    def test_is_available_when_installed(self) -> None:
        """Test is_available returns True when cloudflared is installed."""
        with patch("shutil.which", return_value="/usr/bin/cloudflared"):
            backend = CloudflaredBackend()
            assert backend.is_available() is True

    def test_is_available_when_not_installed(self) -> None:
        """Test is_available returns False when cloudflared is not installed."""
        with patch("shutil.which", return_value=None):
            backend = CloudflaredBackend()
            assert backend.is_available() is False

    def test_binary_property_finds_cloudflared(self) -> None:
        """Test binary property finds cloudflared."""
        with patch("shutil.which", return_value="/usr/local/bin/cloudflared"):
            backend = CloudflaredBackend()
            assert backend.binary == Path("/usr/local/bin/cloudflared")

    def test_binary_property_raises_when_not_found(self) -> None:
        """Test binary property raises when not found."""
        with patch("shutil.which", return_value=None):
            backend = CloudflaredBackend()
            with pytest.raises(CloudflaredNotFoundError):
                _ = backend.binary

    def test_stop_kills_process(self) -> None:
        """Test stop kills the process."""
        with patch("os.kill") as mock_kill:
            backend = CloudflaredBackend()
            backend.stop(12345)
            mock_kill.assert_called_once_with(12345, 15)  # SIGTERM

    def test_stop_ignores_dead_process(self) -> None:
        """Test stop handles already dead process."""
        with patch("os.kill", side_effect=ProcessLookupError):
            backend = CloudflaredBackend()
            # Should not raise
            backend.stop(12345)

    def test_stop_ignores_permission_error(self) -> None:
        """Test stop handles permission error."""
        with patch("os.kill", side_effect=PermissionError):
            backend = CloudflaredBackend()
            # Should not raise
            backend.stop(12345)


class TestTunnelManager:
    """Tests for tunnel manager."""

    def test_list_empty(self, tmp_path: Path) -> None:
        """Test listing with no tunnels."""
        with patch("gaol.tunnel.manager.get_data_dir", return_value=tmp_path):
            manager = TunnelManager()
            tunnels = manager.list()
            assert tunnels == []

    def test_list_filters_by_container(self, tmp_path: Path) -> None:
        """Test listing filters by container name."""
        store_file = tmp_path / "tunnels.yaml"
        store_file.write_text("""
tunnels:
  - id: abc123
    container_name: app1
    container_port: 8080
    host_port: 8080
    public_url: https://test1.trycloudflare.com
    process_pid: 99999999
    started_at: "2024-01-15T10:00:00"
    status: running
  - id: def456
    container_name: app2
    container_port: 3000
    host_port: 3000
    public_url: https://test2.trycloudflare.com
    process_pid: 99999998
    started_at: "2024-01-15T10:01:00"
    status: running
""")

        with (
            patch("gaol.tunnel.manager.get_data_dir", return_value=tmp_path),
            patch.object(TunnelManager, "_is_process_running", return_value=True),
        ):
            manager = TunnelManager()
            tunnels = manager.list("app1")

            assert len(tunnels) == 1
            assert tunnels[0].container_name == "app1"

    def test_get_tunnel(self, tmp_path: Path) -> None:
        """Test getting a tunnel by ID."""
        store_file = tmp_path / "tunnels.yaml"
        store_file.write_text("""
tunnels:
  - id: abc123
    container_name: myapp
    container_port: 8080
    host_port: 8080
    public_url: https://test.trycloudflare.com
    process_pid: 99999999
    started_at: "2024-01-15T10:00:00"
    status: running
""")

        with (
            patch("gaol.tunnel.manager.get_data_dir", return_value=tmp_path),
            patch.object(TunnelManager, "_is_process_running", return_value=True),
        ):
            manager = TunnelManager()
            tunnel = manager.get("abc123")

            assert tunnel is not None
            assert tunnel.id == "abc123"
            assert tunnel.container_name == "myapp"

    def test_get_tunnel_not_found(self, tmp_path: Path) -> None:
        """Test getting a nonexistent tunnel."""
        with patch("gaol.tunnel.manager.get_data_dir", return_value=tmp_path):
            manager = TunnelManager()
            tunnel = manager.get("nonexistent")
            assert tunnel is None

    def test_stop_raises_when_not_found(self, tmp_path: Path) -> None:
        """Test stop raises when tunnel not found."""
        with patch("gaol.tunnel.manager.get_data_dir", return_value=tmp_path):
            manager = TunnelManager()

            with pytest.raises(TunnelNotFoundError) as exc_info:
                manager.stop("nonexistent")

            assert exc_info.value.tunnel_id == "nonexistent"

    def test_stop_all_returns_count(self, tmp_path: Path) -> None:
        """Test stop_all returns count of stopped tunnels."""
        store_file = tmp_path / "tunnels.yaml"
        store_file.write_text("""
tunnels:
  - id: abc123
    container_name: app1
    container_port: 8080
    host_port: 8080
    public_url: https://test1.trycloudflare.com
    process_pid: 99999999
    started_at: "2024-01-15T10:00:00"
    status: running
  - id: def456
    container_name: app2
    container_port: 3000
    host_port: 3000
    public_url: https://test2.trycloudflare.com
    process_pid: 99999998
    started_at: "2024-01-15T10:01:00"
    status: running
""")

        with (
            patch("gaol.tunnel.manager.get_data_dir", return_value=tmp_path),
            patch.object(TunnelManager, "_is_process_running", return_value=True),
            patch.object(CloudflaredBackend, "stop"),
        ):
            manager = TunnelManager()
            count = manager.stop_all()

            assert count == 2

    def test_stop_all_filters_by_container(self, tmp_path: Path) -> None:
        """Test stop_all filters by container."""
        store_file = tmp_path / "tunnels.yaml"
        store_file.write_text("""
tunnels:
  - id: abc123
    container_name: app1
    container_port: 8080
    host_port: 8080
    public_url: https://test1.trycloudflare.com
    process_pid: 99999999
    started_at: "2024-01-15T10:00:00"
    status: running
  - id: def456
    container_name: app2
    container_port: 3000
    host_port: 3000
    public_url: https://test2.trycloudflare.com
    process_pid: 99999998
    started_at: "2024-01-15T10:01:00"
    status: running
""")

        with (
            patch("gaol.tunnel.manager.get_data_dir", return_value=tmp_path),
            patch.object(TunnelManager, "_is_process_running", return_value=True),
            patch.object(CloudflaredBackend, "stop"),
        ):
            manager = TunnelManager()
            count = manager.stop_all("app1")

            assert count == 1

    def test_start_raises_when_tunnel_exists(self, tmp_path: Path) -> None:
        """Test start raises when tunnel already exists."""
        store_file = tmp_path / "tunnels.yaml"
        store_file.write_text("""
tunnels:
  - id: abc123
    container_name: myapp
    container_port: 8080
    host_port: 8080
    public_url: https://test.trycloudflare.com
    process_pid: 99999999
    started_at: "2024-01-15T10:00:00"
    status: running
""")

        with (
            patch("gaol.tunnel.manager.get_data_dir", return_value=tmp_path),
            patch.object(TunnelManager, "_is_process_running", return_value=True),
        ):
            manager = TunnelManager()

            with pytest.raises(TunnelAlreadyExistsError) as exc_info:
                manager.start("myapp", 8080)

            assert exc_info.value.container == "myapp"
            assert exc_info.value.port == 8080

    def test_get_mapped_port_uses_container_port(self, tmp_path: Path) -> None:
        """Test _get_mapped_port uses container port when no mapping."""
        with (
            patch("gaol.tunnel.manager.get_data_dir", return_value=tmp_path),
            patch("gaol.tunnel.manager.get_container_store") as mock_store,
        ):
            mock_store.return_value.get.return_value = None
            manager = TunnelManager()

            port = manager._get_mapped_port("myapp", 8080)
            assert port == 8080

    def test_is_process_running_true(self, tmp_path: Path) -> None:
        """Test _is_process_running returns True for running process."""
        with (
            patch("gaol.tunnel.manager.get_data_dir", return_value=tmp_path),
            patch("os.kill") as mock_kill,
        ):
            mock_kill.return_value = None  # Process exists
            manager = TunnelManager()

            assert manager._is_process_running(12345) is True

    def test_is_process_running_false_when_not_found(self, tmp_path: Path) -> None:
        """Test _is_process_running returns False when process not found."""
        with (
            patch("gaol.tunnel.manager.get_data_dir", return_value=tmp_path),
            patch("os.kill", side_effect=ProcessLookupError),
        ):
            manager = TunnelManager()

            assert manager._is_process_running(12345) is False


class TestTunnelCLI:
    """Tests for tunnel CLI commands."""

    def test_tunnel_help(self) -> None:
        """Test tunnel subcommand help."""
        from gaol.cli import app

        runner = CliRunner()
        result = runner.invoke(app, ["tunnel", "--help"])
        assert result.exit_code == 0
        assert "tunnel" in result.output.lower()

    def test_tunnel_start_help(self) -> None:
        """Test tunnel start help."""
        from gaol.cli import app

        runner = CliRunner()
        result = runner.invoke(app, ["tunnel", "start", "--help"])
        assert result.exit_code == 0
        assert "container" in result.output.lower()

    def test_tunnel_stop_help(self) -> None:
        """Test tunnel stop help."""
        from gaol.cli import app

        runner = CliRunner()
        result = runner.invoke(app, ["tunnel", "stop", "--help"])
        assert result.exit_code == 0

    def test_tunnel_list_help(self) -> None:
        """Test tunnel list help."""
        from gaol.cli import app

        runner = CliRunner()
        result = runner.invoke(app, ["tunnel", "list", "--help"])
        assert result.exit_code == 0

    def test_tunnel_stop_all_help(self) -> None:
        """Test tunnel stop-all help."""
        from gaol.cli import app

        runner = CliRunner()
        result = runner.invoke(app, ["tunnel", "stop-all", "--help"])
        assert result.exit_code == 0

    def test_tunnel_list_empty(self, tmp_path: Path) -> None:
        """Test listing with no tunnels."""
        from gaol.cli import app

        runner = CliRunner()

        with patch("gaol.tunnel.manager.get_data_dir", return_value=tmp_path):
            result = runner.invoke(app, ["tunnel", "list"])

        assert result.exit_code == 0
        assert "No active tunnels" in result.output

    def test_tunnel_stop_not_found(self, tmp_path: Path) -> None:
        """Test stop with nonexistent tunnel."""
        from gaol.cli import app

        runner = CliRunner()

        with patch("gaol.tunnel.manager.get_data_dir", return_value=tmp_path):
            result = runner.invoke(app, ["tunnel", "stop", "nonexistent"])

        assert result.exit_code == 1
        assert "not found" in result.output.lower()

    def test_tunnel_stop_all_empty(self, tmp_path: Path) -> None:
        """Test stop-all with no tunnels."""
        from gaol.cli import app

        runner = CliRunner()

        with patch("gaol.tunnel.manager.get_data_dir", return_value=tmp_path):
            result = runner.invoke(app, ["tunnel", "stop-all"])

        assert result.exit_code == 0
        assert "No tunnels to stop" in result.output


class TestTunnelPersistence:
    """Tests for tunnel YAML persistence."""

    def test_save_and_load_tunnel(self, tmp_path: Path) -> None:
        """Test saving and loading tunnels."""
        with patch("gaol.tunnel.manager.get_data_dir", return_value=tmp_path):
            manager = TunnelManager()

            tunnel = ActiveTunnel(
                id="test123",
                container_name="myapp",
                container_port=8080,
                host_port=8080,
                public_url="https://test.trycloudflare.com",
                process_pid=12345,
            )

            manager._save_tunnel(tunnel)

            # Verify file was created
            store_file = tmp_path / "tunnels.yaml"
            assert store_file.exists()

            # Load and verify
            loaded = manager._load_tunnels()
            assert len(loaded) == 1
            assert loaded[0].id == "test123"
            assert loaded[0].container_name == "myapp"

    def test_remove_tunnel(self, tmp_path: Path) -> None:
        """Test removing a tunnel from store."""
        store_file = tmp_path / "tunnels.yaml"
        store_file.write_text("""
tunnels:
  - id: abc123
    container_name: myapp
    container_port: 8080
    host_port: 8080
    public_url: https://test.trycloudflare.com
    process_pid: 12345
    started_at: "2024-01-15T10:00:00"
    status: running
""")

        with patch("gaol.tunnel.manager.get_data_dir", return_value=tmp_path):
            manager = TunnelManager()
            manager._remove_tunnel("abc123")

            tunnels = manager._load_tunnels()
            assert len(tunnels) == 0

    def test_load_empty_file(self, tmp_path: Path) -> None:
        """Test loading from empty file."""
        store_file = tmp_path / "tunnels.yaml"
        store_file.write_text("")

        with patch("gaol.tunnel.manager.get_data_dir", return_value=tmp_path):
            manager = TunnelManager()
            tunnels = manager._load_tunnels()
            assert tunnels == []

    def test_load_invalid_yaml(self, tmp_path: Path) -> None:
        """Test loading from invalid YAML."""
        store_file = tmp_path / "tunnels.yaml"
        store_file.write_text("invalid: [yaml: content")

        with patch("gaol.tunnel.manager.get_data_dir", return_value=tmp_path):
            manager = TunnelManager()
            tunnels = manager._load_tunnels()
            assert tunnels == []
