"""Tests for ClickHouse preflight detection.

These tests verify the preflight detection system works correctly
without requiring an actual ClickHouse connection.
"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest
from opendeviationbar.clickhouse.preflight import (
    ENV_NOT_CONFIGURED_WARNING,
    ClickHouseNotConfiguredError,
    HostConnection,
    InstallationLevel,
    PreflightResult,
    _find_clickhouse_binary,
    _is_port_open,
    _is_ssh_available,
    _resolve_ssh_alias_to_ip,
    detect_clickhouse_state,
    get_available_clickhouse_host,
)


class TestInstallationLevel:
    """Tests for InstallationLevel enum."""

    def test_level_ordering(self) -> None:
        """Verify level ordering is correct."""
        assert InstallationLevel.ENV_NOT_CONFIGURED < InstallationLevel.NOT_INSTALLED
        assert InstallationLevel.NOT_INSTALLED < InstallationLevel.INSTALLED_NOT_RUNNING
        assert (
            InstallationLevel.INSTALLED_NOT_RUNNING
            < InstallationLevel.RUNNING_NO_SCHEMA
        )
        assert InstallationLevel.RUNNING_NO_SCHEMA < InstallationLevel.FULLY_CONFIGURED

    def test_level_values(self) -> None:
        """Verify level values."""
        assert InstallationLevel.ENV_NOT_CONFIGURED == -1
        assert InstallationLevel.NOT_INSTALLED == 0
        assert InstallationLevel.INSTALLED_NOT_RUNNING == 1
        assert InstallationLevel.RUNNING_NO_SCHEMA == 2
        assert InstallationLevel.FULLY_CONFIGURED == 3


class TestPreflightResult:
    """Tests for PreflightResult dataclass."""

    def test_creation(self) -> None:
        """Test creating a PreflightResult."""
        result = PreflightResult(
            level=InstallationLevel.FULLY_CONFIGURED,
            version="24.3.5",
            binary_path="/usr/bin/clickhouse",
            message="Ready",
        )
        assert result.level == InstallationLevel.FULLY_CONFIGURED
        assert result.version == "24.3.5"
        assert result.binary_path == "/usr/bin/clickhouse"
        assert result.message == "Ready"
        assert result.action_required is None

    def test_defaults(self) -> None:
        """Test default values."""
        result = PreflightResult(level=InstallationLevel.NOT_INSTALLED)
        assert result.version is None
        assert result.binary_path is None
        assert result.message == ""
        assert result.action_required is None


class TestHostConnection:
    """Tests for HostConnection dataclass."""

    def test_local_connection(self) -> None:
        """Test local connection."""
        conn = HostConnection(host="localhost", method="local")
        assert conn.host == "localhost"
        assert conn.method == "local"
        assert conn.port == 8123

    def test_ssh_tunnel_connection(self) -> None:
        """Test SSH tunnel connection."""
        conn = HostConnection(host="gpu-host", method="ssh_tunnel", port=9000)
        assert conn.host == "gpu-host"
        assert conn.method == "ssh_tunnel"
        assert conn.port == 9000


class TestClickHouseNotConfiguredError:
    """Tests for ClickHouseNotConfiguredError."""

    def test_default_message(self) -> None:
        """Test default error message."""
        error = ClickHouseNotConfiguredError()
        assert "OPENDEVIATIONBAR CLICKHOUSE CACHE" in str(error)
        assert "CONFIGURATION REQUIRED" in str(error)
        assert "mise" in str(error)
        assert "OPENDEVIATIONBAR_CH_HOSTS" in str(error)

    def test_custom_message(self) -> None:
        """Test custom error message."""
        error = ClickHouseNotConfiguredError("Custom error")
        assert str(error) == "Custom error"

    def test_warning_content(self) -> None:
        """Verify warning includes setup guidance."""
        assert "~/.ssh/config" in ENV_NOT_CONFIGURED_WARNING
        assert "mise.toml" in ENV_NOT_CONFIGURED_WARNING
        assert "brew install" in ENV_NOT_CONFIGURED_WARNING
        assert "curl https://clickhouse.com/" in ENV_NOT_CONFIGURED_WARNING


class TestIsPortOpen:
    """Tests for _is_port_open function."""

    def test_port_closed(self) -> None:
        """Test detecting closed port."""
        # Use a port that's very unlikely to be open
        assert _is_port_open("localhost", 59999, timeout=0.1) is False

    @patch("socket.socket")
    def test_port_open(self, mock_socket_class: MagicMock) -> None:
        """Test detecting open port."""
        mock_socket = MagicMock()
        mock_socket_class.return_value.__enter__ = MagicMock(return_value=mock_socket)
        mock_socket_class.return_value.__exit__ = MagicMock(return_value=None)
        mock_socket.connect_ex.return_value = 0

        assert _is_port_open("localhost", 8123, timeout=1.0) is True

    @patch("socket.socket")
    def test_connection_error(self, mock_socket_class: MagicMock) -> None:
        """Test handling connection errors."""
        mock_socket_class.side_effect = OSError("Connection refused")
        assert _is_port_open("localhost", 8123) is False


class TestFindClickhouseBinary:
    """Tests for _find_clickhouse_binary function."""

    @patch("shutil.which")
    @patch("pathlib.Path.exists")
    def test_find_in_path(self, mock_exists: MagicMock, mock_which: MagicMock) -> None:
        """Test finding binary in PATH."""
        mock_which.return_value = "/usr/bin/clickhouse-server"
        mock_exists.return_value = False

        result = _find_clickhouse_binary()
        assert result == "/usr/bin/clickhouse-server"

    @patch("shutil.which")
    @patch("pathlib.Path.exists")
    def test_find_in_common_location(
        self, mock_exists: MagicMock, mock_which: MagicMock
    ) -> None:
        """Test finding binary in common location."""
        mock_which.return_value = None
        mock_exists.side_effect = lambda: True  # All paths exist

        # Should find first path that exists
        result = _find_clickhouse_binary()
        # Result depends on which path is checked first
        assert result is not None or result is None  # May or may not find

    @patch("shutil.which")
    @patch("pathlib.Path.exists")
    def test_not_found(self, mock_exists: MagicMock, mock_which: MagicMock) -> None:
        """Test when binary not found."""
        mock_which.return_value = None
        mock_exists.return_value = False

        result = _find_clickhouse_binary()
        assert result is None


class TestResolveSSHAlias:
    """Tests for _resolve_ssh_alias_to_ip function."""

    @patch("subprocess.run")
    def test_resolve_success(self, mock_run: MagicMock) -> None:
        """Test successful SSH alias resolution."""
        mock_run.return_value = MagicMock(
            stdout="hostname 192.168.1.100\nuser myuser\n",
            returncode=0,
        )

        result = _resolve_ssh_alias_to_ip("my-host")
        assert result == "192.168.1.100"

    @patch("subprocess.run")
    def test_resolve_not_found(self, mock_run: MagicMock) -> None:
        """Test when hostname not in output."""
        mock_run.return_value = MagicMock(
            stdout="user myuser\nport 22\n",
            returncode=0,
        )

        result = _resolve_ssh_alias_to_ip("my-host")
        assert result is None

    @patch("subprocess.run")
    def test_resolve_timeout(self, mock_run: MagicMock) -> None:
        """Test handling timeout."""
        import subprocess

        mock_run.side_effect = subprocess.TimeoutExpired("ssh", 2)

        result = _resolve_ssh_alias_to_ip("my-host")
        assert result is None


class TestIsSSHAvailable:
    """Tests for _is_ssh_available function."""

    @patch("subprocess.run")
    def test_ssh_success(self, mock_run: MagicMock) -> None:
        """Test successful SSH check."""
        mock_run.return_value = MagicMock(
            stdout=b"1\n",
            returncode=0,
        )

        assert _is_ssh_available("my-host") is True

    @patch("subprocess.run")
    def test_ssh_failure(self, mock_run: MagicMock) -> None:
        """Test failed SSH check."""
        mock_run.return_value = MagicMock(
            stdout=b"",
            returncode=255,
        )

        assert _is_ssh_available("my-host") is False

    @patch("subprocess.run")
    def test_ssh_timeout(self, mock_run: MagicMock) -> None:
        """Test SSH timeout."""
        import subprocess

        mock_run.side_effect = subprocess.TimeoutExpired("ssh", 8)

        assert _is_ssh_available("my-host") is False


class TestDetectClickhouseState:
    """Tests for detect_clickhouse_state function."""

    @patch("opendeviationbar.clickhouse.preflight._find_clickhouse_binary")
    def test_not_installed(self, mock_find: MagicMock) -> None:
        """Test detecting not installed state."""
        mock_find.return_value = None

        result = detect_clickhouse_state()
        assert result.level == InstallationLevel.NOT_INSTALLED
        assert "not installed" in result.message.lower()
        assert result.action_required is not None
        assert "Install" in result.action_required

    @patch("opendeviationbar.clickhouse.preflight._find_clickhouse_binary")
    @patch("opendeviationbar.clickhouse.preflight._is_port_open")
    def test_installed_not_running(
        self, mock_port: MagicMock, mock_find: MagicMock
    ) -> None:
        """Test detecting installed but not running state."""
        mock_find.return_value = "/usr/bin/clickhouse"
        mock_port.return_value = False

        result = detect_clickhouse_state()
        assert result.level == InstallationLevel.INSTALLED_NOT_RUNNING
        assert result.binary_path == "/usr/bin/clickhouse"
        assert "not running" in result.message.lower()

    @patch("opendeviationbar.clickhouse.preflight._find_clickhouse_binary")
    @patch("opendeviationbar.clickhouse.preflight._is_port_open")
    @patch("opendeviationbar.clickhouse.preflight._get_clickhouse_version")
    @patch("opendeviationbar.clickhouse.preflight._has_opendeviationbar_schema")
    def test_running_no_schema(
        self,
        mock_schema: MagicMock,
        mock_version: MagicMock,
        mock_port: MagicMock,
        mock_find: MagicMock,
    ) -> None:
        """Test detecting running but no schema state."""
        mock_find.return_value = "/usr/bin/clickhouse"
        mock_port.return_value = True
        mock_version.return_value = "24.3.5"
        mock_schema.return_value = False

        result = detect_clickhouse_state()
        assert result.level == InstallationLevel.RUNNING_NO_SCHEMA
        assert result.version == "24.3.5"
        assert "schema missing" in result.message.lower()

    @patch("opendeviationbar.clickhouse.preflight._find_clickhouse_binary")
    @patch("opendeviationbar.clickhouse.preflight._is_port_open")
    @patch("opendeviationbar.clickhouse.preflight._get_clickhouse_version")
    @patch("opendeviationbar.clickhouse.preflight._has_opendeviationbar_schema")
    def test_fully_configured(
        self,
        mock_schema: MagicMock,
        mock_version: MagicMock,
        mock_port: MagicMock,
        mock_find: MagicMock,
    ) -> None:
        """Test detecting fully configured state."""
        mock_find.return_value = "/usr/bin/clickhouse"
        mock_port.return_value = True
        mock_version.return_value = "24.3.5"
        mock_schema.return_value = True

        result = detect_clickhouse_state()
        assert result.level == InstallationLevel.FULLY_CONFIGURED
        assert result.version == "24.3.5"
        assert "ready" in result.message.lower()


class TestGetAvailableClickhouseHost:
    """Tests for get_available_clickhouse_host function.

    IMPORTANT: Every test must explicitly set OPENDEVIATIONBAR_MODE via @patch.dict
    to isolate from the mise environment (which sets OPENDEVIATIONBAR_MODE=remote).
    Without this, tests that assume AUTO mode will fail because REMOTE mode
    short-circuits before checking localhost.
    """

    @patch("opendeviationbar.clickhouse.preflight._verify_clickhouse")
    @patch.dict("os.environ", {"OPENDEVIATIONBAR_MODE": "auto"})
    def test_localhost_available(self, mock_verify: MagicMock) -> None:
        """Test when localhost is available (AUTO mode)."""
        mock_verify.return_value = True

        result = get_available_clickhouse_host()
        assert result.host == "localhost"
        assert result.method == "local"
        assert result.port == 8123

    @patch("opendeviationbar.clickhouse.preflight._verify_clickhouse")
    @patch("opendeviationbar.clickhouse.preflight._is_direct_available")
    @patch.dict(
        "os.environ",
        {
            "OPENDEVIATIONBAR_CH_HOSTS": "gpu1,gpu2",
            "OPENDEVIATIONBAR_CH_PRIMARY": "",
            "OPENDEVIATIONBAR_MODE": "auto",
        },
        clear=True,
    )
    def test_direct_available(
        self, mock_direct: MagicMock, mock_verify: MagicMock
    ) -> None:
        """Test when direct connection available (AUTO mode)."""
        mock_verify.return_value = False  # localhost not available
        mock_direct.side_effect = [True]  # First host works

        result = get_available_clickhouse_host()
        assert result.host == "gpu1"
        assert result.method == "direct"

    @patch("opendeviationbar.clickhouse.preflight._verify_clickhouse")
    @patch("opendeviationbar.clickhouse.preflight._is_direct_available")
    @patch("opendeviationbar.clickhouse.preflight._is_ssh_available")
    @patch.dict(
        "os.environ",
        {
            "OPENDEVIATIONBAR_CH_HOSTS": "gpu1",
            "OPENDEVIATIONBAR_CH_PRIMARY": "",
            "OPENDEVIATIONBAR_MODE": "auto",
        },
        clear=True,
    )
    def test_ssh_tunnel_fallback(
        self, mock_ssh: MagicMock, mock_direct: MagicMock, mock_verify: MagicMock
    ) -> None:
        """Test SSH tunnel fallback (AUTO mode)."""
        mock_verify.return_value = False
        mock_direct.return_value = False
        mock_ssh.return_value = True

        result = get_available_clickhouse_host()
        assert result.host == "gpu1"
        assert result.method == "ssh_tunnel"

    @patch("opendeviationbar.clickhouse.preflight._verify_clickhouse")
    @patch("opendeviationbar.clickhouse.preflight._is_direct_available")
    @patch("opendeviationbar.clickhouse.preflight._is_ssh_available")
    @patch.dict(
        "os.environ",
        {"OPENDEVIATIONBAR_CH_HOSTS": "", "OPENDEVIATIONBAR_MODE": "auto"},
        clear=True,
    )
    def test_no_hosts_available(
        self, mock_ssh: MagicMock, mock_direct: MagicMock, mock_verify: MagicMock
    ) -> None:
        """Test when no hosts available (AUTO mode)."""
        mock_verify.return_value = False
        mock_direct.return_value = False
        mock_ssh.return_value = False

        with pytest.raises(ClickHouseNotConfiguredError) as exc_info:
            get_available_clickhouse_host()

        assert "CONFIGURATION REQUIRED" in str(exc_info.value)

    @patch("opendeviationbar.clickhouse.preflight._is_port_open")
    @patch("opendeviationbar.clickhouse.preflight._is_direct_available")
    @patch("opendeviationbar.clickhouse.preflight._verify_clickhouse")
    @patch.dict(
        "os.environ",
        {
            "OPENDEVIATIONBAR_CH_HOSTS": "gpu1,gpu2",
            "OPENDEVIATIONBAR_CH_PRIMARY": "gpu2",
            "OPENDEVIATIONBAR_MODE": "auto",
        },
    )
    def test_primary_host_preferred(
        self, mock_verify: MagicMock, mock_direct: MagicMock, mock_port: MagicMock
    ) -> None:
        """Test that primary host is tried first (AUTO mode)."""
        mock_port.return_value = False
        mock_verify.return_value = False  # Prevent localhost from short-circuiting
        # Track which hosts are checked
        checked_hosts: list[str] = []

        def track_host(host: str) -> bool:
            checked_hosts.append(host)
            return host == "gpu2"  # Only gpu2 works

        mock_direct.side_effect = track_host

        result = get_available_clickhouse_host()
        assert result.host == "gpu2"
        # gpu2 (primary) should be checked first
        assert checked_hosts[0] == "gpu2"
