"""Tests for gaol service_migration module."""

from __future__ import annotations

from pathlib import Path
from tempfile import NamedTemporaryFile
from unittest.mock import MagicMock, patch

import pytest

from gaol.service_migration import (
    DependencyDiscovery,
    DiscoveredDependencies,
    MigrationAnalysis,
    MigrationWarning,
    MigrationWarningSeverity,
    ParsedSystemdService,
    ServiceInstallSection,
    ServiceMigrator,
    ServiceNotFoundError,
    ServiceSection,
    SpecGenerator,
    SystemdUnitParser,
    TemplateServiceError,
    UnitSection,
    UnsupportedServiceError,
)
from gaol.service_migration.models import WELLKNOWN_SERVICES
from gaol.specs.models import ContainerSpec


class TestModels:
    """Tests for service migration data models."""

    def test_unit_section_defaults(self) -> None:
        """Test UnitSection default values."""
        unit = UnitSection()
        assert unit.description is None
        assert unit.after == []
        assert unit.requires == []

    def test_unit_section_values(self) -> None:
        """Test UnitSection with values."""
        unit = UnitSection(
            description="Test service",
            after=["network.target", "syslog.target"],
            requires=["dbus.socket"],
        )
        assert unit.description == "Test service"
        assert len(unit.after) == 2
        assert "network.target" in unit.after

    def test_service_section_defaults(self) -> None:
        """Test ServiceSection default values."""
        service = ServiceSection()
        assert service.type == "simple"
        assert service.exec_start is None
        assert service.user == "root"
        assert service.restart == "no"
        assert service.environment == {}

    def test_service_section_values(self) -> None:
        """Test ServiceSection with values."""
        service = ServiceSection(
            type="notify",
            exec_start="/usr/sbin/nginx -g 'daemon off;'",
            user="www-data",
            working_directory="/var/www",
            environment={"NODE_ENV": "production"},
            restart="on-failure",
        )
        assert service.type == "notify"
        assert service.exec_start == "/usr/sbin/nginx -g 'daemon off;'"
        assert service.user == "www-data"
        assert service.environment["NODE_ENV"] == "production"

    def test_install_section_defaults(self) -> None:
        """Test ServiceInstallSection default values."""
        install = ServiceInstallSection()
        assert install.wanted_by == []
        assert install.also == []

    def test_parsed_service_get_binary_path(self) -> None:
        """Test get_binary_path extraction."""
        service = ParsedSystemdService(
            name="test",
            file_path=Path("/etc/systemd/system/test.service"),
            service=ServiceSection(exec_start="/usr/bin/python3 -m myapp"),
        )
        assert service.get_binary_path() == "/usr/bin/python3"

    def test_parsed_service_get_binary_path_with_prefix(self) -> None:
        """Test get_binary_path with systemd prefix characters."""
        service = ParsedSystemdService(
            name="test",
            file_path=Path("/etc/systemd/system/test.service"),
            service=ServiceSection(exec_start="-/usr/sbin/nginx"),
        )
        assert service.get_binary_path() == "/usr/sbin/nginx"

    def test_parsed_service_get_binary_path_none(self) -> None:
        """Test get_binary_path when no ExecStart."""
        service = ParsedSystemdService(
            name="test",
            file_path=Path("/etc/systemd/system/test.service"),
        )
        assert service.get_binary_path() is None

    def test_discovered_dependencies_defaults(self) -> None:
        """Test DiscoveredDependencies default values."""
        deps = DiscoveredDependencies()
        assert deps.packages == []
        assert deps.config_files == []
        assert deps.listening_ports == []

    def test_migration_warning_str(self) -> None:
        """Test MigrationWarning string representation."""
        warning = MigrationWarning(
            category="socket_activation",
            message="Service uses socket activation",
            severity=MigrationWarningSeverity.WARNING,
            suggestion="Consider using ports instead",
        )
        s = str(warning)
        assert "[WARN]" in s
        assert "socket_activation" in s
        assert "Suggestion:" in s

    def test_migration_analysis_has_critical_warnings(self) -> None:
        """Test has_critical_warnings property."""
        service = ParsedSystemdService(
            name="test",
            file_path=Path("/etc/systemd/system/test.service"),
        )
        deps = DiscoveredDependencies()

        # No warnings
        analysis = MigrationAnalysis(service=service, dependencies=deps)
        assert not analysis.has_critical_warnings

        # Info warning
        analysis.warnings.append(
            MigrationWarning(
                category="test",
                message="Info",
                severity=MigrationWarningSeverity.INFO,
            )
        )
        assert not analysis.has_critical_warnings

        # Critical warning
        analysis.warnings.append(
            MigrationWarning(
                category="test",
                message="Critical",
                severity=MigrationWarningSeverity.CRITICAL,
            )
        )
        assert analysis.has_critical_warnings


class TestWellKnownServices:
    """Tests for well-known services registry."""

    def test_nginx_defined(self) -> None:
        """Test nginx is in well-known services."""
        assert "nginx" in WELLKNOWN_SERVICES
        nginx = WELLKNOWN_SERVICES["nginx"]
        assert "nginx" in nginx.packages
        assert 80 in nginx.ports
        assert 443 in nginx.ports

    def test_postgresql_defined(self) -> None:
        """Test postgresql is in well-known services."""
        assert "postgresql" in WELLKNOWN_SERVICES
        pg = WELLKNOWN_SERVICES["postgresql"]
        assert 5432 in pg.ports
        assert "/etc/postgresql/" in pg.config_paths

    def test_redis_defined(self) -> None:
        """Test redis-server is in well-known services."""
        assert "redis-server" in WELLKNOWN_SERVICES
        redis = WELLKNOWN_SERVICES["redis-server"]
        assert 6379 in redis.ports


class TestSystemdUnitParser:
    """Tests for systemd unit file parsing."""

    def test_parse_simple_service(self) -> None:
        """Test parsing a simple service file."""
        content = """
[Unit]
Description=Test Service
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/myapp
User=myuser
Restart=on-failure

[Install]
WantedBy=multi-user.target
"""
        with NamedTemporaryFile(mode="w", suffix=".service", delete=False) as f:
            f.write(content)
            f.flush()

            parser = SystemdUnitParser()
            parsed = parser.parse_file(f.name)

            assert parsed.unit.description == "Test Service"
            assert "network.target" in parsed.unit.after
            assert parsed.service.type == "simple"
            assert parsed.service.exec_start == "/usr/bin/myapp"
            assert parsed.service.user == "myuser"
            assert parsed.service.restart == "on-failure"
            assert "multi-user.target" in parsed.install.wanted_by

            Path(f.name).unlink()

    def test_parse_environment_variables(self) -> None:
        """Test parsing environment variables."""
        content = """
[Unit]
Description=Test

[Service]
ExecStart=/usr/bin/myapp
Environment=FOO=bar
Environment="BAZ=qux with spaces"
"""
        with NamedTemporaryFile(mode="w", suffix=".service", delete=False) as f:
            f.write(content)
            f.flush()

            parser = SystemdUnitParser()
            parsed = parser.parse_file(f.name)

            assert "FOO" in parsed.service.environment
            assert parsed.service.environment["FOO"] == "bar"
            assert "BAZ" in parsed.service.environment
            assert parsed.service.environment["BAZ"] == "qux with spaces"

            Path(f.name).unlink()

    def test_parse_list_values(self) -> None:
        """Test parsing multi-value directives."""
        content = """
[Unit]
Description=Test
After=network.target syslog.target
Wants=postgresql.service redis.service

[Service]
ExecStart=/usr/bin/myapp
"""
        with NamedTemporaryFile(mode="w", suffix=".service", delete=False) as f:
            f.write(content)
            f.flush()

            parser = SystemdUnitParser()
            parsed = parser.parse_file(f.name)

            assert len(parsed.unit.after) == 2
            assert "network.target" in parsed.unit.after
            assert "syslog.target" in parsed.unit.after
            assert len(parsed.unit.wants) == 2

            Path(f.name).unlink()

    def test_parse_state_directory(self) -> None:
        """Test parsing StateDirectory directive."""
        content = """
[Unit]
Description=Test

[Service]
ExecStart=/usr/bin/myapp
StateDirectory=myapp
ConfigurationDirectory=myapp
"""
        with NamedTemporaryFile(mode="w", suffix=".service", delete=False) as f:
            f.write(content)
            f.flush()

            parser = SystemdUnitParser()
            parsed = parser.parse_file(f.name)

            assert "myapp" in parsed.service.state_directory
            assert "myapp" in parsed.service.configuration_directory

            Path(f.name).unlink()

    def test_parse_file_not_found(self) -> None:
        """Test parsing non-existent file raises error."""
        parser = SystemdUnitParser()
        with pytest.raises(ServiceNotFoundError):
            parser.parse_file("/nonexistent/path.service")

    def test_find_service_template_error(self) -> None:
        """Test that template services without instance raise error."""
        parser = SystemdUnitParser()
        with pytest.raises(TemplateServiceError):
            parser.find_service_file("foo@.service")


class TestDependencyDiscovery:
    """Tests for dependency discovery."""

    def test_discover_config_files(self) -> None:
        """Test discovering config files from service."""
        service = ParsedSystemdService(
            name="myapp",
            file_path=Path("/etc/systemd/system/myapp.service"),
            service=ServiceSection(
                exec_start="/usr/bin/myapp",
                environment_file=["-/etc/default/myapp"],
                configuration_directory=["myapp"],
            ),
        )

        discovery = DependencyDiscovery(query_system=False)
        deps, warnings = discovery.discover(service)

        # Config discovery runs - may not find files if they don't exist
        assert isinstance(deps, DiscoveredDependencies)

    def test_discover_socket_units(self) -> None:
        """Test discovering socket units."""
        service = ParsedSystemdService(
            name="myapp",
            file_path=Path("/etc/systemd/system/myapp.service"),
            unit=UnitSection(requires=["myapp.socket"]),
        )

        discovery = DependencyDiscovery(query_system=False)
        deps, warnings = discovery.discover(service)

        assert "myapp.socket" in deps.socket_units
        # Should have a warning about socket activation
        socket_warnings = [w for w in warnings if w.category == "socket_activation"]
        assert len(socket_warnings) == 1

    def test_discover_capabilities(self) -> None:
        """Test discovering required capabilities."""
        service = ParsedSystemdService(
            name="myapp",
            file_path=Path("/etc/systemd/system/myapp.service"),
            service=ServiceSection(
                exec_start="/usr/bin/myapp",
                ambient_capabilities=["CAP_NET_BIND_SERVICE", "CAP_NET_RAW"],
            ),
        )

        discovery = DependencyDiscovery(query_system=False)
        deps, warnings = discovery.discover(service)

        assert "CAP_NET_BIND_SERVICE" in deps.required_capabilities
        assert "CAP_NET_RAW" in deps.required_capabilities

    def test_wellknown_service_data_applied(self) -> None:
        """Test that well-known service data is applied."""
        service = ParsedSystemdService(
            name="nginx",
            file_path=Path("/lib/systemd/system/nginx.service"),
            service=ServiceSection(exec_start="/usr/sbin/nginx"),
        )

        discovery = DependencyDiscovery(query_system=False, use_wellknown=True)
        deps, warnings = discovery.discover(service)

        # Should have nginx package and ports from well-known data
        assert "nginx" in deps.packages
        assert 80 in deps.listening_ports
        assert 443 in deps.listening_ports

    def test_system_service_warning(self) -> None:
        """Test that system services get critical warning."""
        service = ParsedSystemdService(
            name="systemd-logind",
            file_path=Path("/lib/systemd/system/systemd-logind.service"),
            service=ServiceSection(exec_start="/lib/systemd/systemd-logind"),
        )

        discovery = DependencyDiscovery(query_system=False)
        deps, warnings = discovery.discover(service)

        critical_warnings = [
            w for w in warnings if w.severity == MigrationWarningSeverity.CRITICAL
        ]
        assert len(critical_warnings) > 0

    @patch("subprocess.run")
    def test_discover_binary_package_dpkg(self, mock_run: MagicMock) -> None:
        """Test discovering package with dpkg."""
        mock_run.side_effect = [
            MagicMock(returncode=0, stdout="/usr/bin/which"),  # which dpkg
            MagicMock(returncode=0, stdout="nginx: /usr/sbin/nginx"),  # dpkg -S
        ]

        discovery = DependencyDiscovery(query_system=True)
        package = discovery.discover_binary_package("/usr/sbin/nginx")

        assert package == "nginx"


class TestSpecGenerator:
    """Tests for container spec generation."""

    def test_generate_basic_spec(self) -> None:
        """Test generating a basic container spec."""
        service = ParsedSystemdService(
            name="myapp",
            file_path=Path("/etc/systemd/system/myapp.service"),
            unit=UnitSection(description="My Application"),
            service=ServiceSection(
                exec_start="/usr/bin/myapp --config /etc/myapp/config.yaml",
                user="myuser",
                working_directory="/var/lib/myapp",
                restart="on-failure",
            ),
        )
        deps = DiscoveredDependencies(
            packages=["myapp"],
            listening_ports=[8080],
        )
        analysis = MigrationAnalysis(
            service=service,
            dependencies=deps,
        )

        generator = SpecGenerator()
        spec = generator.generate(analysis)

        assert spec.name == "myapp"
        assert spec.runtime == "nspawn"
        assert spec.distro == "trixie"
        assert len(spec.services) == 1
        assert spec.services[0].name == "myapp"
        assert spec.services[0].user == "myuser"
        assert spec.network.ports == ["8080:8080"]

    def test_generate_with_volumes(self) -> None:
        """Test generating spec with volume mounts."""
        service = ParsedSystemdService(
            name="myapp",
            file_path=Path("/etc/systemd/system/myapp.service"),
            service=ServiceSection(exec_start="/usr/bin/myapp"),
        )
        deps = DiscoveredDependencies(
            config_files=["/etc/myapp.conf"],
            config_directories=["/etc/myapp"],
            state_directories=["/var/lib/myapp"],
        )
        analysis = MigrationAnalysis(service=service, dependencies=deps)

        # Mock file existence
        with patch.object(Path, "exists", return_value=True):
            generator = SpecGenerator()
            spec = generator.generate(analysis, copy_config=True, copy_data=True)

            # Should have volume mounts
            assert len(spec.volumes) > 0

    def test_generate_with_custom_name(self) -> None:
        """Test generating spec with custom container name."""
        service = ParsedSystemdService(
            name="myapp",
            file_path=Path("/etc/systemd/system/myapp.service"),
            service=ServiceSection(exec_start="/usr/bin/myapp"),
        )
        deps = DiscoveredDependencies()
        analysis = MigrationAnalysis(service=service, dependencies=deps)

        generator = SpecGenerator()
        spec = generator.generate(analysis, container_name="custom-name")

        assert spec.name == "custom-name"

    def test_generate_privileged_mode(self) -> None:
        """Test generating spec with privileged mode."""
        service = ParsedSystemdService(
            name="myapp",
            file_path=Path("/etc/systemd/system/myapp.service"),
            service=ServiceSection(exec_start="/usr/bin/myapp"),
        )
        deps = DiscoveredDependencies()
        analysis = MigrationAnalysis(
            service=service,
            dependencies=deps,
            requires_privileged=True,
        )

        generator = SpecGenerator()
        spec = generator.generate(analysis)

        assert spec.privileged is True

    def test_to_yaml(self) -> None:
        """Test YAML output."""
        spec = ContainerSpec(
            name="test",
            runtime="nspawn",
            distro="trixie",
        )

        generator = SpecGenerator()
        yaml_output = generator.to_yaml(spec)

        assert "name: test" in yaml_output
        assert "Container spec for migrated service" in yaml_output

    def test_sanitize_name(self) -> None:
        """Test container name sanitization."""
        generator = SpecGenerator()

        assert generator._sanitize_name("my-service.service") == "my-service"
        assert generator._sanitize_name("foo@instance.service") == "foo-instance"
        assert generator._sanitize_name("my_service") == "my-service"


class TestServiceMigrator:
    """Tests for the main ServiceMigrator orchestration class."""

    def test_analyze_service(self) -> None:
        """Test analyzing a service."""
        content = """
[Unit]
Description=Test Service

[Service]
ExecStart=/usr/bin/myapp

[Install]
WantedBy=multi-user.target
"""
        with NamedTemporaryFile(mode="w", suffix=".service", delete=False) as f:
            f.write(content)
            f.flush()

            # Create parser that finds our temp file
            parser = SystemdUnitParser(search_paths=[Path(f.name).parent])

            # Analyze by file path
            parsed = parser.parse_file(f.name)
            discovery = DependencyDiscovery(query_system=False)
            deps, warnings = discovery.discover(parsed)

            analysis = MigrationAnalysis(
                service=parsed,
                dependencies=deps,
                warnings=warnings,
            )

            assert analysis.service.name == Path(f.name).name.replace(".service", "")
            assert analysis.can_migrate is True

            Path(f.name).unlink()

    def test_generate_spec_from_analysis(self) -> None:
        """Test generating spec from analysis."""
        service = ParsedSystemdService(
            name="myapp",
            file_path=Path("/etc/systemd/system/myapp.service"),
            service=ServiceSection(
                exec_start="/usr/bin/myapp",
                user="appuser",
            ),
        )
        deps = DiscoveredDependencies(packages=["myapp"])
        analysis = MigrationAnalysis(service=service, dependencies=deps)

        migrator = ServiceMigrator()
        spec = migrator.generate_spec(analysis)

        assert spec.name == "myapp"
        assert len(spec.services) == 1

    def test_generate_spec_blocked_service(self) -> None:
        """Test that blocked services raise error."""
        service = ParsedSystemdService(
            name="blocked",
            file_path=Path("/etc/systemd/system/blocked.service"),
            service=ServiceSection(exec_start="/usr/bin/blocked"),
        )
        deps = DiscoveredDependencies()
        analysis = MigrationAnalysis(
            service=service,
            dependencies=deps,
            can_migrate=False,
            migration_blockers=["Service cannot be migrated"],
        )

        migrator = ServiceMigrator()
        with pytest.raises(UnsupportedServiceError):
            migrator.generate_spec(analysis)

    def test_get_analysis_summary(self) -> None:
        """Test generating analysis summary."""
        service = ParsedSystemdService(
            name="myapp",
            file_path=Path("/etc/systemd/system/myapp.service"),
            unit=UnitSection(description="My Application"),
            service=ServiceSection(exec_start="/usr/bin/myapp"),
        )
        deps = DiscoveredDependencies(
            packages=["myapp", "libfoo"],
            listening_ports=[8080, 8443],
        )
        warnings = [
            MigrationWarning(
                category="test",
                message="Test warning",
                severity=MigrationWarningSeverity.WARNING,
            )
        ]
        analysis = MigrationAnalysis(
            service=service,
            dependencies=deps,
            warnings=warnings,
        )

        migrator = ServiceMigrator()
        summary = migrator.get_analysis_summary(analysis)

        assert "myapp" in summary
        assert "My Application" in summary
        assert "8080" in summary
        assert "Packages:" in summary
        assert "✓ Can be migrated" in summary

    def test_check_unmigrateable_services(self) -> None:
        """Test that system services are marked as unmigrateable."""
        migrator = ServiceMigrator()

        service = ParsedSystemdService(
            name="systemd-logind",
            file_path=Path("/lib/systemd/system/systemd-logind.service"),
            service=ServiceSection(exec_start="/lib/systemd/systemd-logind"),
        )
        deps = DiscoveredDependencies()

        analysis = MigrationAnalysis(service=service, dependencies=deps)
        migrator._check_blockers(analysis)

        assert analysis.can_migrate is False
        assert len(analysis.migration_blockers) > 0


class TestIntegration:
    """Integration tests for the full migration workflow."""

    def test_full_migration_workflow(self) -> None:
        """Test the complete migration workflow."""
        content = """
[Unit]
Description=Web Application Server
After=network.target

[Service]
Type=simple
User=webapp
ExecStart=/usr/bin/webapp serve --port 8080
Environment=NODE_ENV=production
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
"""
        with NamedTemporaryFile(mode="w", suffix=".service", delete=False) as f:
            f.write(content)
            f.flush()

            # Parse
            parser = SystemdUnitParser()
            parsed = parser.parse_file(f.name)

            assert parsed.unit.description == "Web Application Server"
            assert parsed.service.user == "webapp"
            assert parsed.service.restart == "on-failure"

            # Discover
            discovery = DependencyDiscovery(query_system=False)
            deps, warnings = discovery.discover(parsed)

            # Analyze
            analysis = MigrationAnalysis(
                service=parsed,
                dependencies=deps,
                warnings=warnings,
            )
            assert analysis.can_migrate is True

            # Generate spec
            generator = SpecGenerator()
            spec = generator.generate(analysis)

            assert spec.name == Path(f.name).stem
            assert len(spec.services) == 1
            assert spec.services[0].user == "webapp"
            assert "NODE_ENV" in spec.services[0].environment

            # Generate YAML
            yaml_output = generator.to_yaml(spec)
            assert "webapp" in yaml_output

            Path(f.name).unlink()

    def test_migration_with_all_options(self) -> None:
        """Test migration with all customization options."""
        service = ParsedSystemdService(
            name="myservice",
            file_path=Path("/lib/systemd/system/myservice.service"),
            unit=UnitSection(description="My Service"),
            service=ServiceSection(
                exec_start="/usr/bin/myservice",
                user="svcuser",
                working_directory="/opt/myservice",
                environment={"CONFIG": "/etc/myservice.conf"},
            ),
        )
        deps = DiscoveredDependencies(
            packages=["myservice"],
            config_files=["/etc/myservice.conf"],
            listening_ports=[9000],
        )
        analysis = MigrationAnalysis(service=service, dependencies=deps)

        generator = SpecGenerator(
            default_runtime="nspawn",
            default_distro="bookworm",
            default_profiles=["base", "dev"],
        )

        spec = generator.generate(
            analysis,
            container_name="custom-service",
            runtime="nspawn",
            distro="trixie",
            additional_packages=["extra-pkg"],
            additional_ports=[9001],
        )

        assert spec.name == "custom-service"
        assert spec.runtime == "nspawn"
        assert spec.distro == "trixie"
        assert "base" in spec.profiles
        assert 9000 in [int(p.split(":")[0]) for p in spec.network.ports]
        assert 9001 in [int(p.split(":")[0]) for p in spec.network.ports]
