"""Tests for container group orchestration."""

import tempfile
from datetime import datetime
from pathlib import Path

import pytest

from gaol.groups.models import (
    ContainerRef,
    GroupSpec,
    GroupState,
    GroupStatus,
    HealthCheckConfig,
    HealthCheckType,
    MemberStatus,
)
from gaol.groups.resolver import (
    CircularDependencyError,
    DependencyResolver,
    resolve_start_order,
    resolve_stop_order,
)
from gaol.groups.store import GroupStore, StoredGroup
from gaol.models.container import ContainerStatus


class TestContainerRef:
    """Tests for ContainerRef model."""

    def test_basic_container_ref(self) -> None:
        """Test creating a basic container reference."""
        ref = ContainerRef(image="postgres:16")
        assert ref.image == "postgres:16"
        assert ref.runtime == "docker"
        assert ref.depends_on == []
        assert ref.environment == {}

    def test_container_ref_with_dependencies(self) -> None:
        """Test container with dependencies."""
        ref = ContainerRef(
            image="python:3.11",
            depends_on=["db", "redis"],
            environment={"DATABASE_URL": "postgres://db/app"},
            ports=["8000:8000"],
        )
        assert ref.depends_on == ["db", "redis"]
        assert ref.environment["DATABASE_URL"] == "postgres://db/app"
        assert ref.ports == ["8000:8000"]

    def test_container_ref_with_healthcheck(self) -> None:
        """Test container with health check configuration."""
        ref = ContainerRef(
            image="postgres:16",
            healthcheck=HealthCheckConfig(
                type=HealthCheckType.TCP,
                port=5432,
                interval=5,
                retries=3,
            ),
        )
        assert ref.healthcheck is not None
        assert ref.healthcheck.type == HealthCheckType.TCP
        assert ref.healthcheck.port == 5432


class TestHealthCheckConfig:
    """Tests for HealthCheckConfig model."""

    def test_tcp_healthcheck(self) -> None:
        """Test TCP health check requires port."""
        check = HealthCheckConfig(type=HealthCheckType.TCP, port=5432)
        assert check.port == 5432

    def test_tcp_healthcheck_requires_port(self) -> None:
        """Test TCP health check fails without port."""
        with pytest.raises(ValueError, match="TCP health check requires 'port'"):
            HealthCheckConfig(type=HealthCheckType.TCP)

    def test_http_healthcheck(self) -> None:
        """Test HTTP health check with path."""
        check = HealthCheckConfig(
            type=HealthCheckType.HTTP,
            port=8080,
            path="/health",
        )
        assert check.path == "/health"
        assert check.port == 8080

    def test_http_healthcheck_default_path(self) -> None:
        """Test HTTP health check defaults path to /."""
        check = HealthCheckConfig(type=HealthCheckType.HTTP, port=80)
        assert check.path == "/"

    def test_command_healthcheck(self) -> None:
        """Test command health check."""
        check = HealthCheckConfig(
            type=HealthCheckType.COMMAND,
            command=["pg_isready", "-U", "postgres"],
        )
        assert check.command == ["pg_isready", "-U", "postgres"]

    def test_command_healthcheck_requires_command(self) -> None:
        """Test command health check fails without command."""
        with pytest.raises(ValueError, match="Command health check requires 'command'"):
            HealthCheckConfig(type=HealthCheckType.COMMAND)


class TestGroupSpec:
    """Tests for GroupSpec model."""

    def test_basic_group_spec(self) -> None:
        """Test creating a basic group spec."""
        spec = GroupSpec(
            name="test-group",
            containers={
                "db": ContainerRef(image="postgres:16"),
                "api": ContainerRef(image="python:3.11"),
            },
        )
        assert spec.name == "test-group"
        assert len(spec.containers) == 2
        assert spec.shared_network is True
        assert spec.dns_enabled is True

    def test_group_spec_with_dependencies(self) -> None:
        """Test group spec validates dependencies exist."""
        spec = GroupSpec(
            name="web-stack",
            containers={
                "db": ContainerRef(image="postgres:16"),
                "api": ContainerRef(image="python:3.11", depends_on=["db"]),
            },
        )
        assert spec.containers["api"].depends_on == ["db"]

    def test_group_spec_invalid_dependency(self) -> None:
        """Test group spec fails with invalid dependency."""
        with pytest.raises(ValueError, match="does not exist in the group"):
            GroupSpec(
                name="invalid",
                containers={
                    "api": ContainerRef(image="python:3.11", depends_on=["nonexistent"]),
                },
            )

    def test_group_spec_self_dependency(self) -> None:
        """Test group spec fails with self dependency."""
        with pytest.raises(ValueError, match="cannot depend on itself"):
            GroupSpec(
                name="invalid",
                containers={
                    "api": ContainerRef(image="python:3.11", depends_on=["api"]),
                },
            )

    def test_get_container_full_name(self) -> None:
        """Test getting full container name."""
        spec = GroupSpec(
            name="web-stack",
            containers={"db": ContainerRef(image="postgres:16")},
        )
        assert spec.get_container_full_name("db") == "web-stack-db"

    def test_group_spec_name_validation(self) -> None:
        """Test group name validation."""
        with pytest.raises(ValueError):
            GroupSpec(
                name="invalid name!",
                containers={"db": ContainerRef(image="postgres:16")},
            )


class TestGroupStatus:
    """Tests for GroupStatus model."""

    def test_running_count(self) -> None:
        """Test counting running containers."""
        status = GroupStatus(
            name="test",
            state=GroupState.PARTIALLY_RUNNING,
            containers=[
                MemberStatus(
                    name="db",
                    full_name="test-db",
                    state=ContainerStatus.RUNNING,
                    runtime="docker",
                ),
                MemberStatus(
                    name="api",
                    full_name="test-api",
                    state=ContainerStatus.STOPPED,
                    runtime="docker",
                ),
                MemberStatus(
                    name="web",
                    full_name="test-web",
                    state=ContainerStatus.RUNNING,
                    runtime="docker",
                ),
            ],
        )
        assert status.running_count == 2
        assert status.total_count == 3


class TestDependencyResolver:
    """Tests for dependency resolution."""

    def test_no_dependencies(self) -> None:
        """Test resolution with no dependencies."""
        containers = {
            "a": ContainerRef(image="a"),
            "b": ContainerRef(image="b"),
            "c": ContainerRef(image="c"),
        }
        order = resolve_start_order(containers)
        assert len(order) == 3
        assert set(order) == {"a", "b", "c"}

    def test_linear_dependencies(self) -> None:
        """Test resolution with linear dependencies."""
        containers = {
            "a": ContainerRef(image="a"),
            "b": ContainerRef(image="b", depends_on=["a"]),
            "c": ContainerRef(image="c", depends_on=["b"]),
        }
        order = resolve_start_order(containers)
        assert order.index("a") < order.index("b")
        assert order.index("b") < order.index("c")

    def test_multiple_dependencies(self) -> None:
        """Test resolution with multiple dependencies."""
        containers = {
            "db": ContainerRef(image="db"),
            "redis": ContainerRef(image="redis"),
            "api": ContainerRef(image="api", depends_on=["db", "redis"]),
            "web": ContainerRef(image="web", depends_on=["api"]),
        }
        order = resolve_start_order(containers)
        assert order.index("db") < order.index("api")
        assert order.index("redis") < order.index("api")
        assert order.index("api") < order.index("web")

    def test_circular_dependency(self) -> None:
        """Test detection of circular dependencies."""
        containers = {
            "a": ContainerRef(image="a", depends_on=["c"]),
            "b": ContainerRef(image="b", depends_on=["a"]),
            "c": ContainerRef(image="c", depends_on=["b"]),
        }
        with pytest.raises(CircularDependencyError):
            resolve_start_order(containers)

    def test_stop_order_is_reverse(self) -> None:
        """Test stop order is reverse of start order."""
        containers = {
            "a": ContainerRef(image="a"),
            "b": ContainerRef(image="b", depends_on=["a"]),
            "c": ContainerRef(image="c", depends_on=["b"]),
        }
        start_order = resolve_start_order(containers)
        stop_order = resolve_stop_order(containers)
        assert stop_order == list(reversed(start_order))

    def test_resolver_get_dependencies(self) -> None:
        """Test getting direct dependencies."""
        containers = {
            "a": ContainerRef(image="a"),
            "b": ContainerRef(image="b", depends_on=["a"]),
        }
        resolver = DependencyResolver(containers)
        assert resolver.get_dependencies("b") == ["a"]
        assert resolver.get_dependencies("a") == []

    def test_resolver_get_dependents(self) -> None:
        """Test getting containers that depend on a container."""
        containers = {
            "a": ContainerRef(image="a"),
            "b": ContainerRef(image="b", depends_on=["a"]),
            "c": ContainerRef(image="c", depends_on=["a"]),
        }
        resolver = DependencyResolver(containers)
        dependents = resolver.get_dependents("a")
        assert set(dependents) == {"b", "c"}

    def test_resolver_get_all_dependencies(self) -> None:
        """Test getting transitive dependencies."""
        containers = {
            "a": ContainerRef(image="a"),
            "b": ContainerRef(image="b", depends_on=["a"]),
            "c": ContainerRef(image="c", depends_on=["b"]),
        }
        resolver = DependencyResolver(containers)
        all_deps = resolver.get_all_dependencies("c")
        assert all_deps == {"a", "b"}


class TestGroupStore:
    """Tests for GroupStore persistence."""

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

    def test_save_and_get(self, temp_store: GroupStore) -> None:
        """Test saving and retrieving a group."""
        spec = GroupSpec(
            name="test-group",
            containers={
                "db": ContainerRef(image="postgres:16"),
            },
        )
        stored = temp_store.save(spec)
        assert stored.spec.name == "test-group"
        assert stored.state == GroupState.CREATED

        retrieved = temp_store.get("test-group")
        assert retrieved is not None
        assert retrieved.spec.name == "test-group"

    def test_get_nonexistent(self, temp_store: GroupStore) -> None:
        """Test getting a group that doesn't exist."""
        result = temp_store.get("nonexistent")
        assert result is None

    def test_exists(self, temp_store: GroupStore) -> None:
        """Test checking if a group exists."""
        spec = GroupSpec(
            name="test",
            containers={"db": ContainerRef(image="postgres:16")},
        )
        temp_store.save(spec)

        assert temp_store.exists("test")
        assert not temp_store.exists("nonexistent")

    def test_list_groups(self, temp_store: GroupStore) -> None:
        """Test listing stored groups."""
        spec1 = GroupSpec(
            name="alpha",
            containers={"db": ContainerRef(image="postgres:16")},
        )
        spec2 = GroupSpec(
            name="beta",
            containers={"api": ContainerRef(image="python:3.11")},
        )
        temp_store.save(spec1)
        temp_store.save(spec2)

        groups = temp_store.list()
        assert len(groups) == 2
        assert groups[0].spec.name == "alpha"
        assert groups[1].spec.name == "beta"

    def test_delete(self, temp_store: GroupStore) -> None:
        """Test deleting a group."""
        spec = GroupSpec(
            name="test",
            containers={"db": ContainerRef(image="postgres:16")},
        )
        temp_store.save(spec)

        assert temp_store.exists("test")
        result = temp_store.delete("test")
        assert result is True
        assert not temp_store.exists("test")

    def test_update_state(self, temp_store: GroupStore) -> None:
        """Test updating group state."""
        spec = GroupSpec(
            name="test",
            containers={"db": ContainerRef(image="postgres:16")},
        )
        temp_store.save(spec)

        updated = temp_store.update_state(
            "test",
            GroupState.RUNNING,
            network_name="gaol-test",
            container_ids={"db": "abc123"},
        )
        assert updated is not None
        assert updated.state == GroupState.RUNNING
        assert updated.network_name == "gaol-test"
        assert updated.container_ids == {"db": "abc123"}

    def test_update_container_id(self, temp_store: GroupStore) -> None:
        """Test updating a specific container ID."""
        spec = GroupSpec(
            name="test",
            containers={"db": ContainerRef(image="postgres:16")},
        )
        temp_store.save(spec)

        temp_store.update_container_id("test", "db", "new123")

        retrieved = temp_store.get("test")
        assert retrieved is not None
        assert retrieved.container_ids["db"] == "new123"

    def test_save_preserves_created_at(self, temp_store: GroupStore) -> None:
        """Test that saving preserves original created_at."""
        spec = GroupSpec(
            name="test",
            containers={"db": ContainerRef(image="postgres:16")},
        )
        stored1 = temp_store.save(spec)
        original_created = stored1.created_at

        # Save again with updated state
        stored2 = temp_store.save(spec, state=GroupState.RUNNING)
        assert stored2.created_at == original_created

    def test_list_excludes_stopped(self, temp_store: GroupStore) -> None:
        """Test listing can exclude stopped groups."""
        spec1 = GroupSpec(
            name="running",
            containers={"db": ContainerRef(image="postgres:16")},
        )
        spec2 = GroupSpec(
            name="stopped",
            containers={"api": ContainerRef(image="python:3.11")},
        )
        temp_store.save(spec1, state=GroupState.RUNNING)
        temp_store.save(spec2, state=GroupState.STOPPED)

        all_groups = temp_store.list(include_stopped=True)
        assert len(all_groups) == 2

        running_groups = temp_store.list(include_stopped=False)
        assert len(running_groups) == 1
        assert running_groups[0].spec.name == "running"
