"""Tests for container update management."""

import tempfile
import threading
import time
from datetime import datetime, timedelta
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from gaol.distro import PackageManager
from gaol.update.exceptions import (
    ContainerNotFoundError,
    ContainerNotRunningError,
    HealthCheckFailedError,
    PackageManagerError,
    PolicyNotFoundError,
    RollbackError,
    SnapshotError,
    UpdateError,
    UpdateExecutionError,
)
from gaol.update.executor import UpdateExecutor
from gaol.update.models import (
    PackageUpdate,
    RollbackStrategy,
    SchedulerState,
    UpdateCheck,
    UpdateHistory,
    UpdatePolicy,
    UpdateResult,
    UpdateSchedule,
    UpdateScheduleType,
    UpdateStatus,
)
from gaol.update.scheduler import UpdateScheduler
from gaol.update.store import UpdateStore


class TestUpdateModels:
    """Tests for update Pydantic models."""

    def test_update_schedule_defaults(self) -> None:
        """Test UpdateSchedule default values."""
        schedule = UpdateSchedule()
        assert schedule.schedule_type == UpdateScheduleType.MANUAL
        assert schedule.time_of_day == "03:00"
        assert schedule.day_of_week == 0
        assert schedule.day_of_month == 1
        assert schedule.enabled is True

    def test_update_schedule_custom(self) -> None:
        """Test UpdateSchedule with custom values."""
        schedule = UpdateSchedule(
            schedule_type=UpdateScheduleType.WEEKLY,
            time_of_day="04:30",
            day_of_week=5,
            enabled=False,
        )
        assert schedule.schedule_type == UpdateScheduleType.WEEKLY
        assert schedule.time_of_day == "04:30"
        assert schedule.day_of_week == 5
        assert schedule.enabled is False

    def test_update_schedule_time_validation(self) -> None:
        """Test time format validation."""
        # Valid times
        UpdateSchedule(time_of_day="00:00")
        UpdateSchedule(time_of_day="23:59")
        UpdateSchedule(time_of_day="12:30")

        # Invalid times
        with pytest.raises(ValueError):
            UpdateSchedule(time_of_day="25:00")
        with pytest.raises(ValueError):
            UpdateSchedule(time_of_day="12:60")

    def test_update_policy_defaults(self) -> None:
        """Test UpdatePolicy default values."""
        policy = UpdatePolicy(container_name="test")
        assert policy.container_name == "test"
        assert policy.schedule.schedule_type == UpdateScheduleType.MANUAL
        assert policy.update_packages is True
        assert policy.update_base_image is False
        assert policy.rollback_strategy == RollbackStrategy.AUTO
        assert policy.pre_update_snapshot is True
        assert policy.health_check_after is True
        assert policy.auto_restart is True
        assert policy.packages_to_skip == []

    def test_update_policy_custom(self) -> None:
        """Test UpdatePolicy with custom values."""
        schedule = UpdateSchedule(
            schedule_type=UpdateScheduleType.DAILY,
            time_of_day="02:00",
        )
        policy = UpdatePolicy(
            container_name="mycontainer",
            schedule=schedule,
            rollback_strategy=RollbackStrategy.MANUAL,
            packages_to_skip=["kernel", "systemd"],
        )
        assert policy.container_name == "mycontainer"
        assert policy.schedule.schedule_type == UpdateScheduleType.DAILY
        assert policy.rollback_strategy == RollbackStrategy.MANUAL
        assert policy.packages_to_skip == ["kernel", "systemd"]

    def test_package_update_model(self) -> None:
        """Test PackageUpdate model."""
        pkg = PackageUpdate(
            package_name="vim",
            old_version="8.2",
            new_version="9.0",
        )
        assert pkg.package_name == "vim"
        assert pkg.old_version == "8.2"
        assert pkg.new_version == "9.0"
        assert pkg.action == "upgrade"

    def test_update_result_defaults(self) -> None:
        """Test UpdateResult default values."""
        result = UpdateResult(container_name="test")
        assert result.container_name == "test"
        assert result.status == UpdateStatus.PENDING
        assert result.packages_checked == 0
        assert result.packages_upgraded == 0
        assert result.rolled_back is False
        assert result.success is False

    def test_update_result_success(self) -> None:
        """Test UpdateResult success property."""
        result = UpdateResult(container_name="test", status=UpdateStatus.COMPLETED)
        assert result.success is True

        result = UpdateResult(container_name="test", status=UpdateStatus.FAILED)
        assert result.success is False

    def test_update_result_duration(self) -> None:
        """Test UpdateResult duration calculation."""
        start = datetime.now()
        end = start + timedelta(seconds=30)
        result = UpdateResult(
            container_name="test",
            started_at=start,
            completed_at=end,
        )
        assert result.duration_seconds == pytest.approx(30.0, abs=0.1)

    def test_update_check_model(self) -> None:
        """Test UpdateCheck model."""
        packages = [
            PackageUpdate(package_name="vim", new_version="9.0"),
            PackageUpdate(package_name="git", new_version="2.40"),
        ]
        check = UpdateCheck(
            container_name="test",
            updates_available=True,
            package_count=2,
            packages=packages,
        )
        assert check.container_name == "test"
        assert check.updates_available is True
        assert check.package_count == 2
        assert len(check.packages) == 2

    def test_update_history_add_result(self) -> None:
        """Test UpdateHistory adding results."""
        history = UpdateHistory(container_name="test")
        assert history.total_updates == 0

        result1 = UpdateResult(
            container_name="test",
            status=UpdateStatus.COMPLETED,
            completed_at=datetime.now(),
        )
        history.add_result(result1)
        assert history.total_updates == 1
        assert history.successful_updates == 1

        result2 = UpdateResult(
            container_name="test",
            status=UpdateStatus.FAILED,
            completed_at=datetime.now(),
            rolled_back=True,
        )
        history.add_result(result2)
        assert history.total_updates == 2
        assert history.failed_updates == 1
        assert history.rollbacks == 1

    def test_scheduler_state(self) -> None:
        """Test SchedulerState model."""
        state = SchedulerState()
        assert state.running is False
        assert state.started_at is None
        assert state.next_scheduled == {}


class TestUpdateExceptions:
    """Tests for update exceptions."""

    def test_update_error_base(self) -> None:
        """Test base UpdateError."""
        err = UpdateError("test error")
        assert err.message == "test error"
        assert str(err) == "test error"

    def test_container_not_found_error(self) -> None:
        """Test ContainerNotFoundError."""
        err = ContainerNotFoundError("mycontainer")
        assert err.container_name == "mycontainer"
        assert "mycontainer" in str(err)

    def test_container_not_running_error(self) -> None:
        """Test ContainerNotRunningError."""
        err = ContainerNotRunningError("mycontainer")
        assert err.container_name == "mycontainer"
        assert "not running" in str(err)

    def test_package_manager_error(self) -> None:
        """Test PackageManagerError."""
        err = PackageManagerError("apt", "upgrade", "E: Could not get lock")
        assert err.package_manager == "apt"
        assert err.operation == "upgrade"
        assert err.stderr == "E: Could not get lock"
        assert "apt" in str(err)
        assert "upgrade" in str(err)

    def test_health_check_failed_error(self) -> None:
        """Test HealthCheckFailedError."""
        err = HealthCheckFailedError("mycontainer", "timeout")
        assert err.container_name == "mycontainer"
        assert err.output == "timeout"
        assert "mycontainer" in str(err)

    def test_rollback_error(self) -> None:
        """Test RollbackError."""
        err = RollbackError("mycontainer", "snapshot not found")
        assert err.container_name == "mycontainer"
        assert err.reason == "snapshot not found"

    def test_snapshot_error(self) -> None:
        """Test SnapshotError."""
        err = SnapshotError("create", "disk full")
        assert err.operation == "create"
        assert err.reason == "disk full"

    def test_policy_not_found_error(self) -> None:
        """Test PolicyNotFoundError."""
        err = PolicyNotFoundError("mycontainer")
        assert err.container_name == "mycontainer"


class TestUpdateStore:
    """Tests for UpdateStore."""

    @pytest.fixture
    def temp_store(self) -> UpdateStore:
        """Create a temporary store for testing."""
        with tempfile.TemporaryDirectory() as tmpdir:
            yield UpdateStore(store_dir=Path(tmpdir))

    def test_save_and_get_policy(self, temp_store: UpdateStore) -> None:
        """Test saving and retrieving a policy."""
        policy = UpdatePolicy(
            container_name="test",
            schedule=UpdateSchedule(schedule_type=UpdateScheduleType.DAILY),
        )
        temp_store.save_policy(policy)

        retrieved = temp_store.get_policy("test")
        assert retrieved is not None
        assert retrieved.container_name == "test"
        assert retrieved.schedule.schedule_type == UpdateScheduleType.DAILY

    def test_get_nonexistent_policy(self, temp_store: UpdateStore) -> None:
        """Test getting a policy that doesn't exist."""
        result = temp_store.get_policy("nonexistent")
        assert result is None

    def test_delete_policy(self, temp_store: UpdateStore) -> None:
        """Test deleting a policy."""
        policy = UpdatePolicy(container_name="test")
        temp_store.save_policy(policy)

        assert temp_store.policy_exists("test")
        result = temp_store.delete_policy("test")
        assert result is True
        assert not temp_store.policy_exists("test")

    def test_delete_nonexistent_policy(self, temp_store: UpdateStore) -> None:
        """Test deleting a policy that doesn't exist."""
        result = temp_store.delete_policy("nonexistent")
        assert result is False

    def test_list_policies(self, temp_store: UpdateStore) -> None:
        """Test listing all policies."""
        policy1 = UpdatePolicy(container_name="alpha")
        policy2 = UpdatePolicy(container_name="beta")
        temp_store.save_policy(policy1)
        temp_store.save_policy(policy2)

        policies = temp_store.list_policies()
        assert len(policies) == 2
        names = [p.container_name for p in policies]
        assert "alpha" in names
        assert "beta" in names

    def test_save_and_get_history(self, temp_store: UpdateStore) -> None:
        """Test saving and retrieving history."""
        history = UpdateHistory(
            container_name="test",
            total_updates=5,
            successful_updates=4,
        )
        temp_store.save_history(history)

        retrieved = temp_store.get_history("test")
        assert retrieved.container_name == "test"
        assert retrieved.total_updates == 5
        assert retrieved.successful_updates == 4

    def test_get_history_creates_new(self, temp_store: UpdateStore) -> None:
        """Test that get_history creates new history if not exists."""
        history = temp_store.get_history("new")
        assert history.container_name == "new"
        assert history.total_updates == 0

    def test_append_result(self, temp_store: UpdateStore) -> None:
        """Test appending a result to history."""
        result = UpdateResult(
            container_name="test",
            status=UpdateStatus.COMPLETED,
            packages_upgraded=10,
            completed_at=datetime.now(),
        )
        temp_store.append_result(result)

        history = temp_store.get_history("test")
        assert history.total_updates == 1
        assert history.successful_updates == 1
        assert len(history.results) == 1
        assert history.results[0].packages_upgraded == 10


class TestUpdateExecutor:
    """Tests for UpdateExecutor."""

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

    def test_parse_apt_upgradable(self, mock_runtime: MagicMock) -> None:
        """Test parsing APT upgradable packages output."""
        executor = UpdateExecutor(mock_runtime)

        output = """vim/trixie 9.0.1000-1 amd64 [upgradable from: 8.2.4000-1]
git/trixie 1:2.40.0-1 amd64 [upgradable from: 1:2.39.0-1]
curl/trixie 8.0.0-1 amd64 [upgradable from: 7.88.0-1]"""

        packages = executor._parse_upgradable_packages(output, PackageManager.APT)
        assert len(packages) == 3
        assert packages[0].package_name == "vim"
        assert packages[0].new_version == "9.0.1000-1"
        assert packages[0].old_version == "8.2.4000-1"

    def test_parse_dnf_upgradable(self, mock_runtime: MagicMock) -> None:
        """Test parsing DNF upgradable packages output."""
        executor = UpdateExecutor(mock_runtime)

        output = """vim-enhanced.x86_64        9.0.1000-1.fc38        updates
git.x86_64                 2.40.0-1.fc38          updates"""

        packages = executor._parse_upgradable_packages(output, PackageManager.DNF)
        assert len(packages) == 2
        assert packages[0].package_name == "vim-enhanced"
        assert packages[0].new_version == "9.0.1000-1.fc38"

    def test_parse_apk_upgradable(self, mock_runtime: MagicMock) -> None:
        """Test parsing APK upgradable packages output."""
        executor = UpdateExecutor(mock_runtime)

        output = """vim-8.2.4000-r0 < 9.0.1000-r0
git-2.39.0-r0 < 2.40.0-r0"""

        packages = executor._parse_upgradable_packages(output, PackageManager.APK)
        assert len(packages) == 2
        assert packages[0].package_name == "vim"
        assert packages[0].old_version == "8.2.4000-r0"
        assert packages[0].new_version == "9.0.1000-r0"

    def test_parse_pacman_upgradable(self, mock_runtime: MagicMock) -> None:
        """Test parsing Pacman upgradable packages output."""
        executor = UpdateExecutor(mock_runtime)

        output = """vim 8.2.4000-1 -> 9.0.1000-1
git 2.39.0-1 -> 2.40.0-1"""

        packages = executor._parse_upgradable_packages(output, PackageManager.PACMAN)
        assert len(packages) == 2
        assert packages[0].package_name == "vim"
        assert packages[0].old_version == "8.2.4000-1"
        assert packages[0].new_version == "9.0.1000-1"

    def test_parse_empty_output(self, mock_runtime: MagicMock) -> None:
        """Test parsing empty output."""
        executor = UpdateExecutor(mock_runtime)
        packages = executor._parse_upgradable_packages("", PackageManager.APT)
        assert packages == []

    def test_get_upgrade_command_apt(self, mock_runtime: MagicMock) -> None:
        """Test APT upgrade command generation."""
        executor = UpdateExecutor(mock_runtime)
        cmd = executor._get_upgrade_command(PackageManager.APT)
        assert "apt-get upgrade -y" in cmd
        assert "DEBIAN_FRONTEND=noninteractive" in cmd

    def test_get_upgrade_command_with_exclusions(self, mock_runtime: MagicMock) -> None:
        """Test upgrade command with package exclusions."""
        executor = UpdateExecutor(mock_runtime)

        # APT
        cmd = executor._get_upgrade_command(PackageManager.APT, ["kernel", "systemd"])
        assert "apt-mark hold kernel" in cmd
        assert "apt-mark unhold kernel" in cmd

        # DNF
        cmd = executor._get_upgrade_command(PackageManager.DNF, ["kernel"])
        assert "--exclude=kernel" in cmd

        # PACMAN
        cmd = executor._get_upgrade_command(PackageManager.PACMAN, ["kernel"])
        assert "--ignore kernel" in cmd

    def test_check_updates_success(self, mock_runtime: MagicMock) -> None:
        """Test checking for updates successfully."""
        mock_runtime.exec.side_effect = [
            (0, "", ""),  # apt update
            (0, "vim/trixie 9.0 amd64 [upgradable from: 8.2]", ""),  # apt list
        ]

        executor = UpdateExecutor(mock_runtime)
        check = executor.check_updates("container123", "test", "trixie")

        assert check.updates_available is True
        assert check.package_count == 1
        assert len(check.packages) == 1

    def test_check_updates_none_available(self, mock_runtime: MagicMock) -> None:
        """Test checking when no updates available."""
        mock_runtime.exec.side_effect = [
            (0, "", ""),  # apt update
            (0, "", ""),  # apt list (empty)
        ]

        executor = UpdateExecutor(mock_runtime)
        check = executor.check_updates("container123", "test", "trixie")

        assert check.updates_available is False
        assert check.package_count == 0

    def test_check_updates_error(self, mock_runtime: MagicMock) -> None:
        """Test checking updates with error."""
        mock_runtime.exec.return_value = (1, "", "E: Could not get lock")

        executor = UpdateExecutor(mock_runtime)
        check = executor.check_updates("container123", "test", "trixie")

        assert check.error_message is not None
        assert "lock" in check.error_message.lower()


class TestUpdateScheduler:
    """Tests for UpdateScheduler."""

    @pytest.fixture
    def temp_store(self) -> UpdateStore:
        """Create a temporary store for testing."""
        with tempfile.TemporaryDirectory() as tmpdir:
            yield UpdateStore(store_dir=Path(tmpdir))

    def test_scheduler_start_stop(self, temp_store: UpdateStore) -> None:
        """Test starting and stopping the scheduler."""
        scheduler = UpdateScheduler(temp_store, check_interval=1.0)

        assert not scheduler.is_running
        scheduler.start()
        assert scheduler.is_running

        time.sleep(0.1)  # Let it run briefly
        scheduler.stop()
        assert not scheduler.is_running

    def test_scheduler_state(self, temp_store: UpdateStore) -> None:
        """Test scheduler state tracking."""
        scheduler = UpdateScheduler(temp_store, check_interval=1.0)
        scheduler.start()
        time.sleep(0.1)

        state = scheduler.state
        assert state.running is True
        assert state.started_at is not None

        scheduler.stop()
        state = scheduler.state
        assert state.running is False

    def test_calculate_next_run_daily(self, temp_store: UpdateStore) -> None:
        """Test calculating next run for daily schedule."""
        scheduler = UpdateScheduler(temp_store)

        policy = UpdatePolicy(
            container_name="test",
            schedule=UpdateSchedule(
                schedule_type=UpdateScheduleType.DAILY,
                time_of_day="03:00",
            ),
        )
        temp_store.save_policy(policy)

        next_run = scheduler._calculate_next_scheduled_time(policy.schedule)
        assert next_run is not None

        # Should be at 03:00
        assert next_run.hour == 3
        assert next_run.minute == 0

    def test_calculate_next_run_weekly(self, temp_store: UpdateStore) -> None:
        """Test calculating next run for weekly schedule."""
        scheduler = UpdateScheduler(temp_store)

        policy = UpdatePolicy(
            container_name="test",
            schedule=UpdateSchedule(
                schedule_type=UpdateScheduleType.WEEKLY,
                time_of_day="02:00",
                day_of_week=5,  # Saturday
            ),
        )
        temp_store.save_policy(policy)

        next_run = scheduler._calculate_next_scheduled_time(policy.schedule)
        assert next_run is not None
        assert next_run.hour == 2
        assert next_run.minute == 0
        assert next_run.weekday() == 5  # Saturday

    def test_calculate_next_run_monthly(self, temp_store: UpdateStore) -> None:
        """Test calculating next run for monthly schedule."""
        scheduler = UpdateScheduler(temp_store)

        policy = UpdatePolicy(
            container_name="test",
            schedule=UpdateSchedule(
                schedule_type=UpdateScheduleType.MONTHLY,
                time_of_day="04:00",
                day_of_month=15,
            ),
        )
        temp_store.save_policy(policy)

        next_run = scheduler._calculate_next_scheduled_time(policy.schedule)
        assert next_run is not None
        assert next_run.hour == 4
        assert next_run.minute == 0
        assert next_run.day == 15

    def test_trigger_update(self, temp_store: UpdateStore) -> None:
        """Test manually triggering an update."""
        update_called = threading.Event()
        result = UpdateResult(container_name="test", status=UpdateStatus.COMPLETED)

        def on_update(name: str) -> UpdateResult:
            update_called.set()
            return result

        scheduler = UpdateScheduler(temp_store, on_update=on_update)
        triggered_result = scheduler.trigger_update("test")

        assert update_called.is_set()
        assert triggered_result is not None
        assert triggered_result.status == UpdateStatus.COMPLETED

    def test_remove_container(self, temp_store: UpdateStore) -> None:
        """Test removing a container from scheduler state."""
        scheduler = UpdateScheduler(temp_store)

        # Add a scheduled time
        scheduler._state.next_scheduled["test"] = datetime.now()
        assert "test" in scheduler.get_scheduled_updates()

        scheduler.remove_container("test")
        assert "test" not in scheduler.get_scheduled_updates()


class TestUpdateEnums:
    """Tests for update enums."""

    def test_update_schedule_type_values(self) -> None:
        """Test UpdateScheduleType enum values."""
        assert UpdateScheduleType.MANUAL.value == "manual"
        assert UpdateScheduleType.DAILY.value == "daily"
        assert UpdateScheduleType.WEEKLY.value == "weekly"
        assert UpdateScheduleType.MONTHLY.value == "monthly"

    def test_update_status_values(self) -> None:
        """Test UpdateStatus enum values."""
        assert UpdateStatus.PENDING.value == "pending"
        assert UpdateStatus.CHECKING.value == "checking"
        assert UpdateStatus.DOWNLOADING.value == "downloading"
        assert UpdateStatus.APPLYING.value == "applying"
        assert UpdateStatus.VERIFYING.value == "verifying"
        assert UpdateStatus.ROLLING_BACK.value == "rolling_back"
        assert UpdateStatus.COMPLETED.value == "completed"
        assert UpdateStatus.FAILED.value == "failed"
        assert UpdateStatus.SKIPPED.value == "skipped"

    def test_rollback_strategy_values(self) -> None:
        """Test RollbackStrategy enum values."""
        assert RollbackStrategy.AUTO.value == "auto"
        assert RollbackStrategy.MANUAL.value == "manual"
        assert RollbackStrategy.NONE.value == "none"


class TestUpdateHistoryManagement:
    """Tests for update history management."""

    def test_history_limits_results(self) -> None:
        """Test that history limits stored results to 100."""
        history = UpdateHistory(container_name="test")

        # Add 105 results
        for i in range(105):
            result = UpdateResult(
                container_name="test",
                status=UpdateStatus.COMPLETED,
                completed_at=datetime.now(),
            )
            history.add_result(result)

        assert len(history.results) == 100
        assert history.total_updates == 105

    def test_history_tracks_rollbacks(self) -> None:
        """Test that history tracks rollback count."""
        history = UpdateHistory(container_name="test")

        result = UpdateResult(
            container_name="test",
            status=UpdateStatus.FAILED,
            rolled_back=True,
            completed_at=datetime.now(),
        )
        history.add_result(result)

        assert history.rollbacks == 1
        assert history.failed_updates == 1
