"""Tests for the CLI command helpers module."""

from __future__ import annotations

import platform
from datetime import UTC, datetime
from io import StringIO
from pathlib import Path
from unittest.mock import MagicMock, patch

from rich.console import Console

from dekk.detection.ci import CIBuildInfo, CIInfo, CIProvider
from dekk.cli.cli_commands import (
    run_doctor,
    run_env,
    run_version,
)
from dekk.detection.conda import CondaEnvironment
from dekk.core.context import (
    ContextWorkspaceInfo,
    CPUInfo,
    ExecutionContext,
    GPUInfo,
    MemoryInfo,
)
from dekk.detection.detect import PlatformInfo

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


def _make_platform(**overrides) -> PlatformInfo:
    defaults = {"os": "Linux", "arch": "x86_64"}
    defaults.update(overrides)
    return PlatformInfo(**defaults)


def _make_cpu(**overrides) -> CPUInfo:
    defaults = {"model": "TestCPU", "cores": 8, "threads": 16, "frequency_mhz": 3600.0}
    defaults.update(overrides)
    return CPUInfo(**defaults)


def _make_memory(**overrides) -> MemoryInfo:
    defaults = {"total_mb": 16384, "available_mb": 8192, "used_mb": 8192}
    defaults.update(overrides)
    return MemoryInfo(**defaults)


def _make_gpu(**overrides) -> GPUInfo:
    defaults = {
        "vendor": "nvidia",
        "model": "RTX 4090",
        "memory_mb": 24576,
        "driver_version": "535.0",
    }
    defaults.update(overrides)
    return GPUInfo(**defaults)


def _make_workspace(**overrides) -> ContextWorkspaceInfo:
    defaults = {
        "root": Path("/tmp/project"),
        "git_info": None,
        "build_artifacts": [],
        "config_files": [],
    }
    defaults.update(overrides)
    return ContextWorkspaceInfo(**defaults)


def _make_ci_info(is_ci: bool = False, **overrides) -> CIInfo:
    defaults = {
        "is_ci": is_ci,
        "provider": CIProvider(name="github_actions", display_name="GitHub Actions")
        if is_ci
        else None,
        "build": CIBuildInfo(build_id="12345") if is_ci else CIBuildInfo(),
    }
    defaults.update(overrides)
    return CIInfo(**defaults)


def _make_conda(**overrides) -> CondaEnvironment:
    defaults = {"name": "myenv", "prefix": Path("/opt/conda/envs/myenv"), "is_active": True}
    defaults.update(overrides)
    return CondaEnvironment(**defaults)


def _make_context(**overrides) -> ExecutionContext:
    defaults = {
        "platform": _make_platform(),
        "conda_env": None,
        "ci_info": _make_ci_info(),
        "workspace": _make_workspace(),
        "build_system": None,
        "installed_packages": {"numpy": "1.24.0", "pytest": "7.4.0"},
        "system_libraries": [],
        "cpu_info": _make_cpu(),
        "gpu_info": [],
        "memory_info": _make_memory(),
        "env_vars": {"PATH": "/usr/bin", "HOME": "/home/user"},
        "command_line": ["python", "-m", "myapp"],
        "working_dir": Path("/tmp/project"),
        "timestamp": datetime(2026, 1, 1, tzinfo=UTC),
    }
    defaults.update(overrides)
    return ExecutionContext(**defaults)


def _capture_output(func, *args, **kwargs) -> str:
    """Call func while patching the styles module console, return the output."""
    buf = StringIO()
    test_console = Console(file=buf, no_color=True, highlight=False, width=200)
    import dekk.cli.styles as _styles_mod

    orig_console = _styles_mod.console
    orig_internal = _styles_mod._console
    _styles_mod.console = test_console
    _styles_mod._console = test_console
    try:
        with patch("dekk.cli.cli_commands.console", test_console):
            func(*args, **kwargs)
    finally:
        _styles_mod.console = orig_console
        _styles_mod._console = orig_internal
    return buf.getvalue()


# ===========================================================================
# run_doctor
# ===========================================================================


class TestRunDoctor:
    def test_shows_platform_info(self):
        ctx = _make_context()
        out = _capture_output(run_doctor, ctx)
        assert "Platform Information" in out
        assert "Linux" in out
        assert "x86_64" in out

    def test_shows_python_version(self):
        ctx = _make_context()
        out = _capture_output(run_doctor, ctx)
        assert platform.python_version() in out
        assert "Python" in out

    def test_shows_hostname(self):
        ctx = _make_context()
        out = _capture_output(run_doctor, ctx)
        assert "Hostname" in out
        assert platform.node() in out

    def test_no_conda(self):
        ctx = _make_context(conda_env=None)
        out = _capture_output(run_doctor, ctx)
        assert "Conda Environment" not in out

    def test_with_conda(self):
        conda = _make_conda()
        ctx = _make_context(conda_env=conda)
        out = _capture_output(run_doctor, ctx)
        assert "Conda Environment" in out
        assert "myenv" in out
        assert "Prefix" in out

    def test_conda_shows_package_count(self):
        conda = _make_conda()
        ctx = _make_context(
            conda_env=conda,
            installed_packages={"a": "1", "b": "2", "c": "3"},
        )
        out = _capture_output(run_doctor, ctx)
        assert "Packages: 3" in out

    def test_no_ci(self):
        ctx = _make_context(ci_info=_make_ci_info(is_ci=False))
        out = _capture_output(run_doctor, ctx)
        assert "CI Environment" not in out

    def test_with_ci(self):
        ctx = _make_context(ci_info=_make_ci_info(is_ci=True))
        out = _capture_output(run_doctor, ctx)
        assert "CI Environment" in out
        assert "GitHub Actions" in out

    def test_ci_shows_build_id(self):
        ctx = _make_context(ci_info=_make_ci_info(is_ci=True))
        out = _capture_output(run_doctor, ctx)
        assert "Build ID" in out
        assert "12345" in out

    def test_hardware_cpu(self):
        ctx = _make_context(cpu_info=_make_cpu(model="AMD EPYC", cores=64))
        out = _capture_output(run_doctor, ctx)
        assert "Hardware" in out
        assert "AMD EPYC" in out
        assert "64 cores" in out

    def test_hardware_memory(self):
        ctx = _make_context(memory_info=_make_memory(total_mb=32768))
        out = _capture_output(run_doctor, ctx)
        assert "32768 MB" in out

    def test_no_gpu(self):
        ctx = _make_context(gpu_info=[])
        out = _capture_output(run_doctor, ctx)
        assert "GPU:" not in out

    def test_single_gpu(self):
        gpu = _make_gpu(vendor="nvidia", model="RTX 4090")
        ctx = _make_context(gpu_info=[gpu])
        out = _capture_output(run_doctor, ctx)
        assert "nvidia" in out
        assert "RTX 4090" in out

    def test_multiple_gpus(self):
        gpus = [
            _make_gpu(vendor="nvidia", model="A100"),
            _make_gpu(vendor="amd", model="MI300X"),
        ]
        ctx = _make_context(gpu_info=gpus)
        out = _capture_output(run_doctor, ctx)
        assert "A100" in out
        assert "MI300X" in out

    def test_packages_summary(self):
        ctx = _make_context(installed_packages={"a": "1", "b": "2"})
        out = _capture_output(run_doctor, ctx)
        assert "Packages" in out
        assert "2 installed" in out

    def test_no_packages(self):
        ctx = _make_context(installed_packages={})
        out = _capture_output(run_doctor, ctx)
        # Should not show packages section if empty
        assert "installed" not in out

    def test_platform_as_dict(self):
        """Handle platform stored as a dict (from from_dict)."""
        ctx = _make_context(platform={"os": "Linux", "arch": "x86_64"})
        out = _capture_output(run_doctor, ctx)
        # Should still render without error
        assert "Platform Information" in out

    def test_ci_info_none_provider(self):
        """CI info with is_ci=True but provider=None."""
        ci = CIInfo(is_ci=True, provider=None, build=CIBuildInfo())
        ctx = _make_context(ci_info=ci)
        out = _capture_output(run_doctor, ctx)
        assert "CI Environment" in out
        assert "Provider" not in out

    def test_ci_info_no_build_id(self):
        """CI info with is_ci=True but no build_id."""
        ci = CIInfo(
            is_ci=True,
            provider=CIProvider(name="gitlab_ci", display_name="GitLab CI"),
            build=CIBuildInfo(build_id=None),
        )
        ctx = _make_context(ci_info=ci)
        out = _capture_output(run_doctor, ctx)
        assert "GitLab CI" in out
        assert "Build ID" not in out

    def test_darwin_platform(self):
        ctx = _make_context(platform=_make_platform(os="Darwin", arch="arm64"))
        out = _capture_output(run_doctor, ctx)
        assert "Darwin" in out
        assert "arm64" in out

    def test_windows_platform(self):
        ctx = _make_context(platform=_make_platform(os="Windows", arch="x86_64"))
        out = _capture_output(run_doctor, ctx)
        assert "Windows" in out

    def test_cpu_no_frequency(self):
        ctx = _make_context(cpu_info=_make_cpu(frequency_mhz=None))
        out = _capture_output(run_doctor, ctx)
        assert "TestCPU" in out

    def test_zero_memory(self):
        ctx = _make_context(memory_info=_make_memory(total_mb=0))
        out = _capture_output(run_doctor, ctx)
        assert "0 MB" in out


# ===========================================================================
# run_version
# ===========================================================================


class TestRunVersion:
    def _capture(self, app_name, version, context) -> str:
        return _capture_output(run_version, app_name, version, context)

    def test_shows_app_name_and_version(self):
        ctx = _make_context()
        out = self._capture("myapp", "1.2.3", ctx)
        assert "myapp" in out
        assert "1.2.3" in out

    def test_shows_python_version(self):
        ctx = _make_context()
        out = self._capture("myapp", "1.0", ctx)
        assert platform.python_version() in out

    def test_shows_platform(self):
        ctx = _make_context()
        out = self._capture("myapp", "1.0", ctx)
        assert "Linux" in out
        assert "x86_64" in out

    def test_no_app_name(self):
        ctx = _make_context()
        out = self._capture(None, "1.0", ctx)
        # Should not show app line
        assert "version" not in out.split("\n")[0] or "None" not in out

    def test_no_version(self):
        ctx = _make_context()
        out = self._capture("myapp", None, ctx)
        # Still shows python and platform but not "myapp version None"
        assert "Python" in out

    def test_both_none(self):
        ctx = _make_context()
        out = self._capture(None, None, ctx)
        assert "Python" in out
        assert "Platform" in out

    def test_empty_strings(self):
        ctx = _make_context()
        out = self._capture("", "", ctx)
        assert "Python" in out

    def test_darwin_platform(self):
        ctx = _make_context(platform=_make_platform(os="Darwin", arch="arm64"))
        out = self._capture("tool", "2.0", ctx)
        assert "Darwin" in out
        assert "arm64" in out

    def test_platform_as_dict(self):
        ctx = _make_context(platform={"os": "Linux", "arch": "aarch64"})
        out = self._capture("tool", "1.0", ctx)
        # Should not crash; shows fallback
        assert "Platform" in out


# ===========================================================================
# run_env
# ===========================================================================


class TestRunEnv:
    def _capture(self, context) -> str:
        return _capture_output(run_env, context)

    def test_shows_env_vars_table(self):
        ctx = _make_context(env_vars={"HOME": "/home/user", "PATH": "/usr/bin"})
        out = self._capture(ctx)
        assert "Environment" in out
        assert "Variables" in out
        assert "HOME" in out
        assert "PATH" in out

    def test_env_vars_sorted(self):
        ctx = _make_context(env_vars={"ZEBRA": "z", "ALPHA": "a"})
        out = self._capture(ctx)
        alpha_pos = out.index("ALPHA")
        zebra_pos = out.index("ZEBRA")
        assert alpha_pos < zebra_pos

    def test_empty_env_vars(self):
        ctx = _make_context(env_vars={})
        out = self._capture(ctx)
        # Table title may word-wrap when the table is narrow (no rows)
        assert "Environment" in out
        assert "Variables" in out

    def test_shows_installed_packages(self):
        ctx = _make_context(installed_packages={"numpy": "1.24.0", "pytest": "7.4.0"})
        out = self._capture(ctx)
        assert "Installed Packages" in out
        assert "numpy" in out
        assert "1.24.0" in out

    def test_packages_sorted(self):
        ctx = _make_context(installed_packages={"zlib": "1.0", "attrs": "23.0"})
        out = self._capture(ctx)
        attrs_pos = out.index("attrs")
        zlib_pos = out.index("zlib")
        assert attrs_pos < zlib_pos

    def test_packages_limited_to_20(self):
        pkgs = {f"pkg-{i:03d}": f"{i}.0" for i in range(30)}
        ctx = _make_context(installed_packages=pkgs)
        out = self._capture(ctx)
        # First 20 sorted packages should appear
        assert "pkg-000" in out
        assert "pkg-019" in out
        # 21st onward should not appear
        assert "pkg-020" not in out

    def test_no_packages(self):
        ctx = _make_context(installed_packages={})
        out = self._capture(ctx)
        assert "Installed Packages" in out

    def test_env_var_values_displayed(self):
        ctx = _make_context(env_vars={"MY_VAR": "/some/path/value"})
        out = self._capture(ctx)
        assert "/some/path/value" in out

    def test_long_env_var(self):
        long_val = "x" * 500
        ctx = _make_context(env_vars={"LONG": long_val})
        out = self._capture(ctx)
        assert "LONG" in out

    def test_special_chars_in_env_var(self):
        ctx = _make_context(env_vars={"SPECIAL": "val=with=equals&and!bang"})
        out = self._capture(ctx)
        assert "SPECIAL" in out


# ===========================================================================
# Integration / edge cases
# ===========================================================================


class TestEdgeCases:
    def test_run_doctor_returns_none(self):
        ctx = _make_context()
        # Just verify it runs without error
        _capture_output(run_doctor, ctx)

    def test_run_version_returns_none(self):
        ctx = _make_context()
        _capture_output(run_version, "app", "1.0", ctx)

    def test_run_env_returns_none(self):
        ctx = _make_context()
        _capture_output(run_env, ctx)

    def test_doctor_full_context(self):
        """Test with all optional fields populated."""
        ctx = _make_context(
            conda_env=_make_conda(),
            ci_info=_make_ci_info(is_ci=True),
            gpu_info=[_make_gpu()],
            installed_packages={"pkg": "1.0"},
        )
        out = _capture_output(run_doctor, ctx)
        assert "Conda Environment" in out
        assert "CI Environment" in out
        assert "GPU" in out
        assert "Packages" in out
        assert "1 installed" in out

    def test_doctor_minimal_context(self):
        """Test with minimal context (no conda, no CI, no GPU, no packages)."""
        ctx = _make_context(
            conda_env=None,
            ci_info=_make_ci_info(is_ci=False),
            gpu_info=[],
            installed_packages={},
        )
        out = _capture_output(run_doctor, ctx)
        assert "Platform Information" in out
        assert "Hardware" in out
        assert "Conda Environment" not in out
        assert "CI Environment" not in out

    def test_conda_without_prefix(self):
        """CondaEnvironment-like object without prefix attribute."""
        mock_conda = MagicMock()
        mock_conda.name = "testenv"
        del mock_conda.prefix  # remove prefix attr
        ctx = _make_context(conda_env=mock_conda)
        out = _capture_output(run_doctor, ctx)
        assert "testenv" in out
        assert "Prefix" not in out

    def test_ci_provider_as_string(self):
        """Provider that is just a string (e.g. from deserialization)."""
        ci = MagicMock()
        ci.is_ci = True
        ci.provider = "SomeCI"
        ci.build = CIBuildInfo(build_id="99")
        ctx = _make_context(ci_info=ci)
        out = _capture_output(run_doctor, ctx)
        assert "CI Environment" in out
