"""Tests for Nix integration module."""

from __future__ import annotations

import json
from unittest.mock import MagicMock, patch

import pytest

from gaol.nix.exceptions import (
    FlakeInitError,
    FlakeNotFoundError,
    NixInstallationError,
    NixNotInstalledError,
    NixPackageError,
    NixShellError,
)
from gaol.nix.flake import FlakeManager, get_flake_manager
from gaol.nix.installer import NixInstaller, get_nix_installer
from gaol.nix.models import (
    NixChannel,
    NixConfig,
    NixEnvironment,
    NixFlake,
    NixStatus,
)
from gaol.nix.packages import (
    NIX_PACKAGE_MAPPINGS,
    NIX_SKIP_PACKAGES,
    translate_packages_to_nix,
    translate_to_nix,
    validate_nix_packages,
)
from gaol.nix.shell import NixShell, get_nix_shell


class TestNixExceptions:
    """Test Nix exception classes."""

    def test_nix_not_installed_error(self) -> None:
        """Test NixNotInstalledError message."""
        error = NixNotInstalledError("mycontainer")
        assert "mycontainer" in str(error)
        assert "nix init" in str(error).lower()

    def test_nix_installation_error(self) -> None:
        """Test NixInstallationError message."""
        error = NixInstallationError("mycontainer", "Permission denied")
        assert "mycontainer" in str(error)
        assert "Permission denied" in str(error)

    def test_nix_package_error(self) -> None:
        """Test NixPackageError message."""
        error = NixPackageError(["vim", "git"], "Package not found")
        assert "vim" in str(error)
        assert "git" in str(error)
        assert "Package not found" in str(error)

    def test_nix_shell_error(self) -> None:
        """Test NixShellError message."""
        error = NixShellError("Failed to start shell")
        assert "Failed to start shell" in str(error)

    def test_flake_not_found_error(self) -> None:
        """Test FlakeNotFoundError message."""
        error = FlakeNotFoundError("/app")
        assert "/app" in str(error)
        assert "flake.nix" in str(error).lower()

    def test_flake_init_error(self) -> None:
        """Test FlakeInitError message."""
        error = FlakeInitError("/app", "Directory not writable")
        assert "/app" in str(error)
        assert "Directory not writable" in str(error)


class TestNixModels:
    """Test Nix data models."""

    def test_nix_channel_enum(self) -> None:
        """Test NixChannel enum values."""
        assert NixChannel.UNSTABLE.value == "nixpkgs-unstable"
        assert NixChannel.STABLE_24_11.value == "nixos-24.11"

    def test_nix_config_defaults(self) -> None:
        """Test NixConfig default values."""
        config = NixConfig()
        assert config.enabled is False
        assert config.channel == "nixpkgs-unstable"
        assert config.packages == []
        assert "nix-command" in config.experimental_features
        assert "flakes" in config.experimental_features

    def test_nix_config_get_nix_conf(self) -> None:
        """Test NixConfig.get_nix_conf() generation."""
        config = NixConfig(
            experimental_features=["nix-command", "flakes"],
            max_jobs=8,
            cores=4,
        )
        conf = config.get_nix_conf()
        assert "experimental-features = nix-command flakes" in conf
        assert "max-jobs = 8" in conf
        assert "cores = 4" in conf

    def test_nix_config_with_binary_caches(self) -> None:
        """Test NixConfig with binary caches."""
        config = NixConfig(
            binary_caches=["https://cache.example.com"],
            trusted_public_keys=["cache.example.com:ABC123"],
        )
        conf = config.get_nix_conf()
        assert "https://cache.example.com" in conf
        assert "cache.example.com:ABC123" in conf

    def test_nix_flake_defaults(self) -> None:
        """Test NixFlake default values."""
        flake = NixFlake()
        assert flake.description == "Gaol development environment"
        assert "nixos-24.11" in flake.nixpkgs_ref
        assert flake.packages == []

    def test_nix_flake_generate(self) -> None:
        """Test NixFlake.generate() output."""
        flake = NixFlake(
            description="Test environment",
            packages=["python3", "nodejs"],
            shell_hook="echo 'Hello'",
            env_vars={"MY_VAR": "value"},
        )
        content = flake.generate()

        assert 'description = "Test environment"' in content
        assert "python3" in content
        assert "nodejs" in content
        assert "shellHook" in content
        assert "MY_VAR" in content

    def test_nix_flake_generate_with_inputs(self) -> None:
        """Test NixFlake.generate() with extra inputs."""
        flake = NixFlake(
            packages=["vim"],
            inputs={"rust-overlay": "github:oxalica/rust-overlay"},
        )
        content = flake.generate()

        assert "rust-overlay" in content
        assert "github:oxalica/rust-overlay" in content

    def test_nix_environment_defaults(self) -> None:
        """Test NixEnvironment default values."""
        env = NixEnvironment(name="test")
        assert env.name == "test"
        assert env.packages == []
        assert env.pure is False
        assert env.command is None

    def test_nix_environment_get_shell_command(self) -> None:
        """Test NixEnvironment.get_shell_command()."""
        env = NixEnvironment(
            name="test",
            packages=["python3", "vim"],
            pure=True,
            command="python --version",
        )
        cmd = env.get_shell_command()

        assert "nix-shell" in cmd
        assert "--pure" in cmd
        assert "-p python3" in cmd
        assert "-p vim" in cmd
        assert "--run" in cmd
        assert "python --version" in cmd

    def test_nix_environment_get_develop_command(self) -> None:
        """Test NixEnvironment.get_develop_command()."""
        env = NixEnvironment(name="test", command="bash")
        cmd = env.get_develop_command("/app")

        assert "nix develop" in cmd
        assert "/app" in cmd
        assert "--command" in cmd
        assert "bash" in cmd

    def test_nix_status_defaults(self) -> None:
        """Test NixStatus default values."""
        status = NixStatus()
        assert status.installed is False
        assert status.version is None
        assert status.store_size == 0

    def test_nix_status_to_dict(self) -> None:
        """Test NixStatus.to_dict()."""
        status = NixStatus(
            installed=True,
            version="2.18.1",
            channel="nixpkgs-unstable",
            store_size=1024 * 1024 * 100,  # 100 MB
            num_packages=10,
            flake_support=True,
        )
        data = status.to_dict()

        assert data["installed"] is True
        assert data["version"] == "2.18.1"
        assert data["store_size_mb"] == 100.0
        assert data["num_packages"] == 10
        assert data["flake_support"] is True


class TestNixPackages:
    """Test package name translation."""

    def test_translate_to_nix_known_package(self) -> None:
        """Test translating known packages."""
        assert translate_to_nix("python3-pip") == "python3Packages.pip"
        assert translate_to_nix("fd-find") == "fd"
        assert translate_to_nix("build-essential") == "gcc"

    def test_translate_to_nix_unknown_package(self) -> None:
        """Test translating unknown packages (pass-through)."""
        assert translate_to_nix("someunknownpackage") == "someunknownpackage"

    def test_translate_to_nix_skip_package(self) -> None:
        """Test skipping packages that aren't needed in Nix."""
        assert translate_to_nix("locales") is None
        assert translate_to_nix("apt-transport-https") is None

    def test_translate_packages_to_nix(self) -> None:
        """Test batch package translation."""
        packages = ["python3", "vim", "locales", "git"]
        result = translate_packages_to_nix(packages)

        assert "python3" in result
        assert "vim" in result
        assert "git" in result
        assert len(result) == 3  # locales should be skipped

    def test_translate_packages_deduplication(self) -> None:
        """Test that duplicate packages are removed."""
        packages = ["vim", "vim", "git"]
        result = translate_packages_to_nix(packages)

        assert result.count("vim") == 1
        assert len(result) == 2

    def test_validate_nix_packages(self) -> None:
        """Test package name validation."""
        packages = ["python3", "vim", "invalid$pkg", "good-pkg"]
        valid, invalid = validate_nix_packages(packages)

        assert "python3" in valid
        assert "vim" in valid
        assert "good-pkg" in valid
        assert "invalid$pkg" in invalid

    def test_mappings_not_empty(self) -> None:
        """Test that package mappings are populated."""
        assert len(NIX_PACKAGE_MAPPINGS) > 50  # Should have many mappings
        assert len(NIX_SKIP_PACKAGES) > 0


class TestNixInstaller:
    """Test NixInstaller class."""

    @pytest.fixture
    def installer(self) -> NixInstaller:
        """Create a NixInstaller instance."""
        return NixInstaller()

    @pytest.fixture
    def mock_runtime(self) -> MagicMock:
        """Create a mock runtime."""
        runtime = MagicMock()
        return runtime

    def test_is_installed_true(
        self, installer: NixInstaller, mock_runtime: MagicMock
    ) -> None:
        """Test is_installed returns True when Nix is present."""
        mock_runtime.exec.return_value = (0, "installed", "")

        result = installer.is_installed(mock_runtime, "container1")

        assert result is True
        mock_runtime.exec.assert_called_once()

    def test_is_installed_false(
        self, installer: NixInstaller, mock_runtime: MagicMock
    ) -> None:
        """Test is_installed returns False when Nix is not present."""
        mock_runtime.exec.return_value = (0, "nix_missing", "")

        result = installer.is_installed(mock_runtime, "container1")

        assert result is False

    def test_install_success(
        self, installer: NixInstaller, mock_runtime: MagicMock
    ) -> None:
        """Test successful Nix installation."""
        mock_runtime.exec.return_value = (0, "Installation complete", "")

        result = installer.install(mock_runtime, "container1")

        assert result is True
        assert mock_runtime.exec.call_count >= 1

    def test_install_failure(
        self, installer: NixInstaller, mock_runtime: MagicMock
    ) -> None:
        """Test failed Nix installation."""
        mock_runtime.exec.return_value = (1, "", "Permission denied")

        with pytest.raises(NixInstallationError):
            installer.install(mock_runtime, "container1")

    def test_get_status_not_installed(
        self, installer: NixInstaller, mock_runtime: MagicMock
    ) -> None:
        """Test get_status when Nix is not installed."""
        mock_runtime.exec.return_value = (0, "nix_missing", "")

        status = installer.get_status(mock_runtime, "container1")

        assert status.installed is False

    def test_get_status_installed(
        self, installer: NixInstaller, mock_runtime: MagicMock
    ) -> None:
        """Test get_status when Nix is installed."""
        # Mock various exec calls for status checks
        def mock_exec(container_id, cmd, **kwargs):
            cmd_str = " ".join(cmd) if isinstance(cmd, list) else cmd
            if "command -v nix" in cmd_str:
                return (0, "installed", "")
            elif "nix --version" in cmd_str:
                return (0, "nix (Nix) 2.18.1", "")
            elif "nix-daemon.sh" in cmd_str:
                return (0, "single", "")
            elif "nix-channel --list" in cmd_str:
                return (0, "nixpkgs", "")
            elif "du -sb" in cmd_str:
                return (0, "104857600", "")  # 100 MB
            elif "nix-env -q" in cmd_str:
                return (0, "5", "")
            elif "nix flake --help" in cmd_str:
                return (0, "yes", "")
            return (0, "", "")

        mock_runtime.exec.side_effect = mock_exec

        status = installer.get_status(mock_runtime, "container1")

        assert status.installed is True

    def test_garbage_collect(
        self, installer: NixInstaller, mock_runtime: MagicMock
    ) -> None:
        """Test garbage collection."""
        mock_runtime.exec.return_value = (0, "freed 100 MiB", "")

        freed, output = installer.garbage_collect(mock_runtime, "container1")

        assert "freed" in output.lower()

    def test_get_nix_installer_singleton(self) -> None:
        """Test that get_nix_installer returns singleton."""
        installer1 = get_nix_installer()
        installer2 = get_nix_installer()
        assert installer1 is installer2


class TestNixShell:
    """Test NixShell class."""

    @pytest.fixture
    def shell(self) -> NixShell:
        """Create a NixShell instance."""
        return NixShell()

    @pytest.fixture
    def mock_runtime(self) -> MagicMock:
        """Create a mock runtime."""
        runtime = MagicMock()
        return runtime

    def test_run_shell_not_installed(
        self, shell: NixShell, mock_runtime: MagicMock
    ) -> None:
        """Test run_shell when Nix is not installed."""
        with patch.object(shell._installer, "is_installed", return_value=False):
            with pytest.raises(NixNotInstalledError):
                shell.run_shell(mock_runtime, "container1", ["python3"])

    def test_run_shell_success(
        self, shell: NixShell, mock_runtime: MagicMock
    ) -> None:
        """Test successful shell execution."""
        mock_runtime.exec.return_value = (0, "Python 3.11.0", "")

        with patch.object(shell._installer, "is_installed", return_value=True):
            exit_code, stdout, stderr = shell.run_shell(
                mock_runtime,
                "container1",
                packages=["python311"],
                command="python --version",
            )

        assert exit_code == 0
        assert "Python" in stdout

    def test_run_shell_with_pure(
        self, shell: NixShell, mock_runtime: MagicMock
    ) -> None:
        """Test shell with pure mode."""
        mock_runtime.exec.return_value = (0, "", "")

        with patch.object(shell._installer, "is_installed", return_value=True):
            shell.run_shell(
                mock_runtime,
                "container1",
                packages=["vim"],
                pure=True,
            )

        # Check that --pure was passed
        call_args = mock_runtime.exec.call_args
        cmd = call_args[0][1]  # Second positional arg is command list
        assert any("--pure" in str(c) for c in cmd)

    def test_run_shell_no_packages(
        self, shell: NixShell, mock_runtime: MagicMock
    ) -> None:
        """Test run_shell with no packages raises error."""
        with patch.object(shell._installer, "is_installed", return_value=True):
            with pytest.raises(NixShellError):
                shell.run_shell(mock_runtime, "container1", packages=[])

    def test_install_packages_success(
        self, shell: NixShell, mock_runtime: MagicMock
    ) -> None:
        """Test successful package installation."""
        mock_runtime.exec.return_value = (0, "installing vim...", "")

        with patch.object(shell._installer, "is_installed", return_value=True):
            exit_code, stdout, stderr = shell.install_packages(
                mock_runtime,
                "container1",
                packages=["vim", "git"],
            )

        assert exit_code == 0

    def test_install_packages_failure(
        self, shell: NixShell, mock_runtime: MagicMock
    ) -> None:
        """Test failed package installation."""
        mock_runtime.exec.return_value = (1, "", "Package not found")

        with patch.object(shell._installer, "is_installed", return_value=True):
            with pytest.raises(NixPackageError):
                shell.install_packages(
                    mock_runtime,
                    "container1",
                    packages=["nonexistent"],
                )

    def test_list_installed(
        self, shell: NixShell, mock_runtime: MagicMock
    ) -> None:
        """Test listing installed packages."""
        mock_runtime.exec.return_value = (0, "vim-9.0\ngit-2.40\n", "")

        with patch.object(shell._installer, "is_installed", return_value=True):
            packages = shell.list_installed(mock_runtime, "container1")

        assert "vim-9.0" in packages
        assert "git-2.40" in packages

    def test_get_nix_shell_singleton(self) -> None:
        """Test that get_nix_shell returns singleton."""
        shell1 = get_nix_shell()
        shell2 = get_nix_shell()
        assert shell1 is shell2


class TestFlakeManager:
    """Test FlakeManager class."""

    @pytest.fixture
    def manager(self) -> FlakeManager:
        """Create a FlakeManager instance."""
        return FlakeManager()

    @pytest.fixture
    def mock_runtime(self) -> MagicMock:
        """Create a mock runtime."""
        runtime = MagicMock()
        return runtime

    def test_generate_flake(self, manager: FlakeManager) -> None:
        """Test flake.nix generation."""
        content = manager.generate_flake(
            packages=["python311", "nodejs"],
            description="Test env",
        )

        assert 'description = "Test env"' in content
        assert "python311" in content
        assert "nodejs" in content
        assert "mkShell" in content
        assert "devShells.default" in content

    def test_generate_flake_with_shell_hook(self, manager: FlakeManager) -> None:
        """Test flake generation with shell hook."""
        content = manager.generate_flake(
            packages=["vim"],
            shell_hook="echo 'Welcome!'",
        )

        assert "shellHook" in content
        assert "Welcome" in content

    def test_generate_flake_with_env_vars(self, manager: FlakeManager) -> None:
        """Test flake generation with environment variables."""
        content = manager.generate_flake(
            packages=["vim"],
            env_vars={"MY_VAR": "myvalue"},
        )

        assert "MY_VAR" in content
        assert "myvalue" in content

    def test_init_flake_already_exists(
        self, manager: FlakeManager, mock_runtime: MagicMock
    ) -> None:
        """Test init_flake when flake already exists."""
        mock_runtime.exec.return_value = (0, "exists", "")

        with pytest.raises(FlakeInitError, match="already exists"):
            manager.init_flake(
                mock_runtime,
                "container1",
                "/app",
                packages=["vim"],
            )

    def test_init_flake_success(
        self, manager: FlakeManager, mock_runtime: MagicMock
    ) -> None:
        """Test successful flake initialization."""
        # Return "nix_missing" to avoid matching "exists" in the check
        mock_runtime.exec.return_value = (0, "nix_missing", "")

        manager.init_flake(
            mock_runtime,
            "container1",
            "/app",
            packages=["python311"],
            force=False,
        )

        assert mock_runtime.exec.call_count >= 2

    def test_init_flake_force(
        self, manager: FlakeManager, mock_runtime: MagicMock
    ) -> None:
        """Test init_flake with force overwrites existing."""
        mock_runtime.exec.return_value = (0, "", "")

        manager.init_flake(
            mock_runtime,
            "container1",
            "/app",
            packages=["vim"],
            force=True,
        )

        # Should not check if exists when force=True
        assert mock_runtime.exec.call_count >= 1

    def test_update_flake_not_found(
        self, manager: FlakeManager, mock_runtime: MagicMock
    ) -> None:
        """Test update_flake when flake doesn't exist."""
        mock_runtime.exec.return_value = (0, "not_found", "")

        with pytest.raises(FlakeNotFoundError):
            manager.update_flake(mock_runtime, "container1", "/app")

    def test_develop_not_found(
        self, manager: FlakeManager, mock_runtime: MagicMock
    ) -> None:
        """Test develop when flake doesn't exist."""
        mock_runtime.exec.return_value = (0, "not_found", "")

        with pytest.raises(FlakeNotFoundError):
            manager.develop(mock_runtime, "container1", "/app")

    def test_develop_success(
        self, manager: FlakeManager, mock_runtime: MagicMock
    ) -> None:
        """Test successful nix develop."""
        mock_runtime.exec.side_effect = [
            (0, "exists", ""),  # Check if exists
            (0, "entered shell", ""),  # nix develop
        ]

        exit_code, stdout, stderr = manager.develop(
            mock_runtime, "container1", "/app", command="bash"
        )

        assert exit_code == 0

    def test_check_flake_not_exists(
        self, manager: FlakeManager, mock_runtime: MagicMock
    ) -> None:
        """Test check_flake when flake doesn't exist."""
        mock_runtime.exec.return_value = (0, "no_flake\nno_lock", "")

        status = manager.check_flake(mock_runtime, "container1", "/app")

        assert status["exists"] is False
        assert status["has_lock"] is False

    def test_check_flake_exists(
        self, manager: FlakeManager, mock_runtime: MagicMock
    ) -> None:
        """Test check_flake when flake exists and is valid."""
        mock_runtime.exec.side_effect = [
            (0, "flake_exists\nlock_exists", ""),  # Check files
            (0, "", ""),  # nix flake check
        ]

        status = manager.check_flake(mock_runtime, "container1", "/app")

        assert status["exists"] is True
        assert status["has_lock"] is True
        assert status["valid"] is True

    def test_get_flake_manager_singleton(self) -> None:
        """Test that get_flake_manager returns singleton."""
        manager1 = get_flake_manager()
        manager2 = get_flake_manager()
        assert manager1 is manager2


class TestNixModuleExports:
    """Test module exports."""

    def test_all_exports_importable(self) -> None:
        """Test that all __all__ exports are importable."""
        from gaol.nix import (
            FlakeDevelopError,
            FlakeError,
            FlakeInitError,
            FlakeManager,
            FlakeNotFoundError,
            FlakeUpdateError,
            NixChannel,
            NixChannelError,
            NixConfig,
            NixEnvironment,
            NixError,
            NixFlake,
            NixGarbageCollectionError,
            NixInstallationError,
            NixInstaller,
            NixNotInstalledError,
            NixPackageError,
            NixShell,
            NixShellError,
            NixStatus,
            get_flake_manager,
            get_nix_installer,
            get_nix_shell,
            translate_packages_to_nix,
            translate_to_nix,
        )

        # Basic checks
        assert NixError is not None
        assert NixConfig is not None
        assert NixInstaller is not None
        assert NixShell is not None
        assert FlakeManager is not None


class TestNixIntegration:
    """Integration-style tests."""

    def test_full_workflow_mock(self) -> None:
        """Test full workflow with mocked runtime."""
        from gaol.nix import (
            get_flake_manager,
            get_nix_installer,
            get_nix_shell,
        )

        mock_runtime = MagicMock()
        mock_runtime.exec.return_value = (0, "success", "")

        installer = get_nix_installer()
        shell = get_nix_shell()
        flake_mgr = get_flake_manager()

        # Simulate workflow
        with patch.object(installer, "is_installed", return_value=True):
            with patch.object(shell._installer, "is_installed", return_value=True):
                # Run shell command
                exit_code, stdout, stderr = shell.run_shell(
                    mock_runtime,
                    "container1",
                    packages=["python311"],
                    command="python --version",
                )
                assert exit_code == 0

    def test_package_translation_workflow(self) -> None:
        """Test translating packages for Nix workflow."""
        # Common dev packages
        packages = [
            "python3",
            "python3-pip",
            "nodejs",
            "build-essential",
            "git",
            "vim",
            "curl",
            "locales",  # Should be skipped
        ]

        nix_packages = translate_packages_to_nix(packages)

        assert "python3" in nix_packages
        assert "python3Packages.pip" in nix_packages
        assert "nodejs" in nix_packages
        assert "gcc" in nix_packages  # build-essential maps to gcc
        assert "git" in nix_packages
        assert "vim" in nix_packages
        assert "curl" in nix_packages
        # locales should be skipped
        assert "locales" not in nix_packages

    def test_flake_generation_complete(self) -> None:
        """Test complete flake generation."""
        from gaol.nix import NixFlake

        flake = NixFlake(
            description="Complete dev environment",
            nixpkgs_ref="github:NixOS/nixpkgs/nixos-24.11",
            packages=["python311", "nodejs", "rustc", "cargo"],
            shell_hook='export MYVAR="hello"',
            env_vars={"EDITOR": "vim", "PAGER": "less"},
            inputs={"rust-overlay": "github:oxalica/rust-overlay"},
        )

        content = flake.generate()

        # Check all sections present
        assert 'description = "Complete dev environment"' in content
        assert "github:NixOS/nixpkgs/nixos-24.11" in content
        assert "python311" in content
        assert "nodejs" in content
        assert "rustc" in content
        assert "cargo" in content
        assert "shellHook" in content
        assert "MYVAR" in content
        assert 'EDITOR = "vim"' in content
        assert 'PAGER = "less"' in content
        assert "rust-overlay" in content
        assert "github:oxalica/rust-overlay" in content

        # Check structure
        assert "inputs = {" in content
        assert "outputs = {" in content
        assert "devShells.default" in content
        assert "pkgs.mkShell" in content
