"""Tests for VenomQA Doctor - System health checks and diagnostics."""

from __future__ import annotations

import json
import sys
from unittest.mock import MagicMock, patch

import pytest
from click.testing import CliRunner

from venomqa.cli.doctor import (
    HealthCheck,
    check_config_file,
    check_docker,
    check_docker_compose,
    check_git,
    check_graphviz,
    check_journeys_directory,
    check_package,
    check_postgresql_client,
    check_python_version,
    doctor,
    get_health_checks,
    run_health_checks,
)


class TestHealthCheck:
    """Tests for the HealthCheck class."""

    def test_health_check_initialization(self):
        """Test HealthCheck can be initialized with name, function, and required flag."""
        def check_fn() -> tuple[bool, str]:
            return True, "All good"

        check = HealthCheck("test_check", check_fn, required=True)

        assert check.name == "test_check"
        assert check.check_fn == check_fn
        assert check.required is True

    def test_health_check_default_required(self):
        """Test HealthCheck defaults to required=True."""
        check = HealthCheck("test", lambda: (True, "ok"))
        assert check.required is True

    def test_health_check_optional(self):
        """Test HealthCheck can be marked as optional."""
        check = HealthCheck("test", lambda: (True, "ok"), required=False)
        assert check.required is False

    def test_health_check_run_success(self):
        """Test HealthCheck.run() returns success tuple."""
        check = HealthCheck("test", lambda: (True, "Success message"))
        success, message = check.run()

        assert success is True
        assert message == "Success message"

    def test_health_check_run_failure(self):
        """Test HealthCheck.run() returns failure tuple."""
        check = HealthCheck("test", lambda: (False, "Failure message"))
        success, message = check.run()

        assert success is False
        assert message == "Failure message"

    def test_health_check_run_catches_exception(self):
        """Test HealthCheck.run() catches exceptions and returns failure."""
        def failing_check() -> tuple[bool, str]:
            raise RuntimeError("Something went wrong")

        check = HealthCheck("test", failing_check)
        success, message = check.run()

        assert success is False
        assert "Something went wrong" in message


class TestCheckPythonVersion:
    """Tests for the check_python_version function."""

    def test_python_version_current(self):
        """Test check_python_version with current Python version."""
        success, message = check_python_version()

        # Since we're running tests, Python version should be >= 3.10
        assert success is True
        assert f"Python {sys.version_info.major}.{sys.version_info.minor}" in message

    def test_python_version_310(self):
        """Test check_python_version with Python 3.10."""
        # Create a mock version_info that behaves like the real sys.version_info
        class MockVersionInfo:
            major = 3
            minor = 10
            micro = 0

            def __ge__(self, other):
                return (self.major, self.minor) >= other[:2]

        with patch.object(sys, "version_info", MockVersionInfo()):
            success, message = check_python_version()

        assert success is True
        assert "Python 3.10" in message

    def test_python_version_312(self):
        """Test check_python_version with Python 3.12."""

        class MockVersionInfo:
            major = 3
            minor = 12
            micro = 1

            def __ge__(self, other):
                return (self.major, self.minor) >= other[:2]

        with patch.object(sys, "version_info", MockVersionInfo()):
            success, message = check_python_version()

        assert success is True
        assert "Python 3.12.1" in message

    def test_python_version_too_old(self):
        """Test check_python_version fails with Python < 3.10."""

        class MockVersionInfo:
            major = 3
            minor = 9
            micro = 0

            def __ge__(self, other):
                return (self.major, self.minor) >= other[:2]

        with patch.object(sys, "version_info", MockVersionInfo()):
            success, message = check_python_version()

        assert success is False
        assert "requires >= 3.10" in message


class TestCheckDocker:
    """Tests for the check_docker function."""

    @patch("shutil.which")
    def test_docker_not_in_path(self, mock_which):
        """Test check_docker when Docker is not in PATH."""
        mock_which.return_value = None
        success, message = check_docker()

        assert success is False
        assert "not found in PATH" in message

    @patch("shutil.which")
    @patch("subprocess.run")
    def test_docker_version_fails(self, mock_run, mock_which):
        """Test check_docker when docker --version fails."""
        mock_which.return_value = "/usr/bin/docker"
        mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="Error")

        success, message = check_docker()

        assert success is False
        assert "check failed" in message

    @patch("shutil.which")
    @patch("subprocess.run")
    def test_docker_daemon_not_running(self, mock_run, mock_which):
        """Test check_docker when Docker daemon is not running."""
        mock_which.return_value = "/usr/bin/docker"

        def run_side_effect(cmd, **kwargs):
            if cmd[1] == "--version":
                return MagicMock(returncode=0, stdout="Docker version 24.0.0")
            elif cmd[1] == "info":
                return MagicMock(returncode=1)
            return MagicMock(returncode=1)

        mock_run.side_effect = run_side_effect

        success, message = check_docker()

        assert success is False
        assert "daemon not running" in message

    @patch("shutil.which")
    @patch("subprocess.run")
    def test_docker_running(self, mock_run, mock_which):
        """Test check_docker when Docker is fully functional."""
        mock_which.return_value = "/usr/bin/docker"

        def run_side_effect(cmd, **kwargs):
            if cmd[1] == "--version":
                return MagicMock(returncode=0, stdout="Docker version 24.0.0")
            elif cmd[1] == "info":
                return MagicMock(returncode=0)
            return MagicMock(returncode=0)

        mock_run.side_effect = run_side_effect

        success, message = check_docker()

        assert success is True
        assert "Docker version" in message


class TestCheckDockerCompose:
    """Tests for the check_docker_compose function."""

    @patch("subprocess.run")
    def test_docker_compose_v2(self, mock_run):
        """Test check_docker_compose finds Docker Compose v2."""
        mock_run.return_value = MagicMock(
            returncode=0,
            stdout="Docker Compose version v2.20.0",
        )

        success, message = check_docker_compose()

        assert success is True
        assert "Docker Compose" in message

    @patch("subprocess.run")
    @patch("shutil.which")
    def test_docker_compose_v1_fallback(self, mock_which, mock_run):
        """Test check_docker_compose falls back to docker-compose v1."""

        def run_side_effect(cmd, **kwargs):
            if cmd[0] == "docker" and cmd[1] == "compose":
                return MagicMock(returncode=1)
            elif cmd[0] == "docker-compose":
                return MagicMock(
                    returncode=0,
                    stdout="docker-compose version 1.29.2",
                )
            return MagicMock(returncode=1)

        mock_run.side_effect = run_side_effect
        mock_which.return_value = "/usr/bin/docker-compose"

        success, message = check_docker_compose()

        assert success is True
        assert "docker-compose" in message

    @patch("subprocess.run")
    @patch("shutil.which")
    def test_docker_compose_not_found(self, mock_which, mock_run):
        """Test check_docker_compose when neither version is found."""
        mock_run.return_value = MagicMock(returncode=1)
        mock_which.return_value = None

        success, message = check_docker_compose()

        assert success is False
        assert "not found" in message


class TestCheckPackage:
    """Tests for the check_package function."""

    def test_check_installed_package(self):
        """Test check_package with an installed package."""
        success, message = check_package("click")

        assert success is True
        assert "click" in message

    def test_check_missing_package(self):
        """Test check_package with a missing package."""
        success, message = check_package("nonexistent_package_xyz")

        assert success is False
        assert "not installed" in message

    def test_check_package_with_version(self):
        """Test check_package returns version when available."""
        # rich has __version__
        success, message = check_package("rich")

        assert success is True
        assert "rich" in message


class TestCheckGraphviz:
    """Tests for the check_graphviz function."""

    @patch("shutil.which")
    def test_graphviz_not_installed(self, mock_which):
        """Test check_graphviz when not installed."""
        mock_which.return_value = None

        success, message = check_graphviz()

        assert success is False
        assert "not installed" in message
        assert "optional" in message

    @patch("shutil.which")
    @patch("subprocess.run")
    def test_graphviz_installed(self, mock_run, mock_which):
        """Test check_graphviz when installed."""
        mock_which.return_value = "/usr/bin/dot"
        mock_run.return_value = MagicMock(
            returncode=0,
            stdout="",
            stderr="dot - graphviz version 2.43.0",
        )

        success, message = check_graphviz()

        assert success is True
        assert "graphviz" in message.lower()


class TestCheckPostgresqlClient:
    """Tests for the check_postgresql_client function."""

    @patch("shutil.which")
    def test_psql_not_installed(self, mock_which):
        """Test check_postgresql_client when not installed."""
        mock_which.return_value = None

        success, message = check_postgresql_client()

        assert success is False
        assert "not found" in message
        assert "optional" in message

    @patch("shutil.which")
    @patch("subprocess.run")
    def test_psql_installed(self, mock_run, mock_which):
        """Test check_postgresql_client when installed."""
        mock_which.return_value = "/usr/bin/psql"
        mock_run.return_value = MagicMock(
            returncode=0,
            stdout="psql (PostgreSQL) 15.3",
        )

        success, message = check_postgresql_client()

        assert success is True
        assert "psql" in message


class TestCheckGit:
    """Tests for the check_git function."""

    @patch("shutil.which")
    def test_git_not_installed(self, mock_which):
        """Test check_git when not installed."""
        mock_which.return_value = None

        success, message = check_git()

        assert success is False
        assert "not found" in message

    @patch("shutil.which")
    @patch("subprocess.run")
    def test_git_installed(self, mock_run, mock_which):
        """Test check_git when installed."""
        mock_which.return_value = "/usr/bin/git"
        mock_run.return_value = MagicMock(
            returncode=0,
            stdout="git version 2.40.0",
        )

        success, message = check_git()

        assert success is True
        assert "git" in message.lower()


class TestCheckConfigFile:
    """Tests for the check_config_file function."""

    def test_config_file_not_found(self, tmp_path, monkeypatch):
        """Test check_config_file when no config file exists."""
        monkeypatch.chdir(tmp_path)

        success, message = check_config_file()

        assert success is False
        assert "No venomqa.yaml found" in message

    def test_config_file_found(self, tmp_path, monkeypatch):
        """Test check_config_file when venomqa.yaml exists."""
        monkeypatch.chdir(tmp_path)
        (tmp_path / "venomqa.yaml").write_text("base_url: http://localhost:8000")

        success, message = check_config_file()

        assert success is True
        assert "venomqa.yaml" in message

    def test_config_file_alternate_name(self, tmp_path, monkeypatch):
        """Test check_config_file with alternate config file names."""
        monkeypatch.chdir(tmp_path)
        (tmp_path / "venomqa.yml").write_text("base_url: http://localhost:8000")

        success, message = check_config_file()

        assert success is True
        assert "venomqa.yml" in message


class TestCheckJourneysDirectory:
    """Tests for the check_journeys_directory function."""

    def test_journeys_dir_not_found(self, tmp_path, monkeypatch):
        """Test check_journeys_directory when directory doesn't exist."""
        monkeypatch.chdir(tmp_path)

        success, message = check_journeys_directory()

        assert success is False
        assert "not found" in message

    def test_journeys_dir_empty(self, tmp_path, monkeypatch):
        """Test check_journeys_directory when directory is empty."""
        monkeypatch.chdir(tmp_path)
        (tmp_path / "journeys").mkdir()

        success, message = check_journeys_directory()

        assert success is False
        assert "no journey files found" in message

    def test_journeys_dir_with_files(self, tmp_path, monkeypatch):
        """Test check_journeys_directory when directory has journey files."""
        monkeypatch.chdir(tmp_path)
        journeys_dir = tmp_path / "journeys"
        journeys_dir.mkdir()
        (journeys_dir / "test_journey.py").write_text("# journey")
        (journeys_dir / "another_journey.py").write_text("# journey")

        success, message = check_journeys_directory()

        assert success is True
        assert "2 journey file(s)" in message

    def test_journeys_dir_ignores_underscore_files(self, tmp_path, monkeypatch):
        """Test check_journeys_directory ignores files starting with underscore."""
        monkeypatch.chdir(tmp_path)
        journeys_dir = tmp_path / "journeys"
        journeys_dir.mkdir()
        (journeys_dir / "__init__.py").write_text("")
        (journeys_dir / "_utils.py").write_text("")
        (journeys_dir / "real_journey.py").write_text("# journey")

        success, message = check_journeys_directory()

        assert success is True
        assert "1 journey file(s)" in message


class TestGetHealthChecks:
    """Tests for the get_health_checks function."""

    def test_returns_list_of_health_checks(self):
        """Test get_health_checks returns a list of HealthCheck instances."""
        checks = get_health_checks()

        assert isinstance(checks, list)
        assert len(checks) > 0
        assert all(isinstance(c, HealthCheck) for c in checks)

    def test_required_checks_are_first(self):
        """Test that required checks come before optional checks."""
        checks = get_health_checks()

        # Find the first optional check
        first_optional_idx = None
        for i, check in enumerate(checks):
            if not check.required:
                first_optional_idx = i
                break

        if first_optional_idx is not None:
            # All checks before the first optional should be required
            for i in range(first_optional_idx):
                assert checks[i].required is True

    def test_includes_essential_checks(self):
        """Test that essential checks are included."""
        checks = get_health_checks()
        check_names = [c.name for c in checks]

        assert "Python Version" in check_names
        assert "Docker" in check_names
        assert "Docker Compose" in check_names


class TestRunHealthChecks:
    """Tests for the run_health_checks function."""

    def test_run_health_checks_all_pass(self):
        """Test run_health_checks when all checks pass."""
        checks = [
            HealthCheck("check1", lambda: (True, "passed"), required=True),
            HealthCheck("check2", lambda: (True, "passed"), required=False),
        ]

        passed, failed_required, failed_optional = run_health_checks(checks)

        assert passed == 2
        assert failed_required == 0
        assert failed_optional == 0

    def test_run_health_checks_required_fails(self):
        """Test run_health_checks when a required check fails."""
        checks = [
            HealthCheck("check1", lambda: (False, "failed"), required=True),
            HealthCheck("check2", lambda: (True, "passed"), required=False),
        ]

        passed, failed_required, failed_optional = run_health_checks(checks)

        assert passed == 1
        assert failed_required == 1
        assert failed_optional == 0

    def test_run_health_checks_optional_fails(self):
        """Test run_health_checks when an optional check fails."""
        checks = [
            HealthCheck("check1", lambda: (True, "passed"), required=True),
            HealthCheck("check2", lambda: (False, "failed"), required=False),
        ]

        passed, failed_required, failed_optional = run_health_checks(checks)

        assert passed == 1
        assert failed_required == 0
        assert failed_optional == 1


class TestDoctorCommand:
    """Tests for the doctor CLI command."""

    def test_doctor_command_exits_with_0_on_success(self):
        """Test doctor command exits with 0 when all required checks pass."""
        runner = CliRunner()

        # Mock all checks to pass
        with patch(
            "venomqa.cli.doctor.get_health_checks",
            return_value=[
                HealthCheck("test", lambda: (True, "ok"), required=True),
            ],
        ):
            result = runner.invoke(doctor)

        assert result.exit_code == 0

    def test_doctor_command_exits_with_1_on_failure(self):
        """Test doctor command exits with 1 when a required check fails."""
        runner = CliRunner()

        # Mock a required check to fail
        with patch(
            "venomqa.cli.doctor.get_health_checks",
            return_value=[
                HealthCheck("test", lambda: (False, "failed"), required=True),
            ],
        ):
            result = runner.invoke(doctor)

        assert result.exit_code == 1

    def test_doctor_command_json_output(self):
        """Test doctor command with --json flag outputs valid JSON."""
        runner = CliRunner()

        with patch(
            "venomqa.cli.doctor.get_health_checks",
            return_value=[
                HealthCheck("test1", lambda: (True, "ok"), required=True),
                HealthCheck("test2", lambda: (False, "failed"), required=False),
            ],
        ):
            result = runner.invoke(doctor, ["--json"])

        # Should be valid JSON
        output = json.loads(result.output)

        assert "checks" in output
        assert "summary" in output
        assert len(output["checks"]) == 2
        assert output["summary"]["passed"] == 1
        assert output["summary"]["failed_optional"] == 1
        assert output["summary"]["ready"] is True

    def test_doctor_command_json_output_on_failure(self):
        """Test doctor command with --json flag shows failures correctly."""
        runner = CliRunner()

        with patch(
            "venomqa.cli.doctor.get_health_checks",
            return_value=[
                HealthCheck("test", lambda: (False, "failed"), required=True),
            ],
        ):
            result = runner.invoke(doctor, ["--json"])

        output = json.loads(result.output)

        assert output["summary"]["failed_required"] == 1
        assert output["summary"]["ready"] is False
        assert result.exit_code == 1

    def test_doctor_command_shows_fix_suggestions_on_failure(self):
        """Test doctor command shows fix suggestions when checks fail."""
        runner = CliRunner()

        with patch(
            "venomqa.cli.doctor.get_health_checks",
            return_value=[
                HealthCheck("test", lambda: (False, "failed"), required=True),
            ],
        ):
            result = runner.invoke(doctor)

        assert "To fix:" in result.output or "pip install" in result.output


class TestDoctorIntegration:
    """Integration tests for the doctor system."""

    def test_doctor_runs_all_checks(self):
        """Test that doctor runs all registered health checks."""
        runner = CliRunner()

        result = runner.invoke(doctor, ["--json"])

        if result.exit_code not in (0, 1):
            pytest.fail(f"Unexpected exit code: {result.exit_code}")

        output = json.loads(result.output)
        checks = get_health_checks()

        assert len(output["checks"]) == len(checks)

    def test_doctor_check_names_match(self):
        """Test that JSON output check names match registered checks."""
        runner = CliRunner()

        result = runner.invoke(doctor, ["--json"])
        output = json.loads(result.output)

        expected_names = {c.name for c in get_health_checks()}
        actual_names = {c["name"] for c in output["checks"]}

        assert expected_names == actual_names
