"""
Static regression tests for the Compose migration gate.

These tests parse docker-compose.yml (via PyYAML) and assert the structural
invariants that ensure Alembic runs before any application process starts.
No Docker daemon is required — they are pure-Python configuration checks.

Requirements covered:
  CONN-04 — Cloud connections require an up-to-date schema before use
  CLOUD-01 — cloud_items/cloud_folder_states tables must exist at runtime
"""
from __future__ import annotations

import os
import pathlib

import pytest
import yaml


REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
COMPOSE_PATH = REPO_ROOT / "docker-compose.yml"

# When running inside the Docker container the repo root is not mounted;
# skip the whole module rather than erroring with FileNotFoundError.
if not COMPOSE_PATH.exists():
    pytest.skip(
        f"docker-compose.yml not found at {COMPOSE_PATH} — "
        "these static analysis tests must run from the host repo root, not inside a container.",
        allow_module_level=True,
    )

APPLICATION_SERVICES = {"backend", "celery-worker", "celery-beat"}


@pytest.fixture(scope="module")
def compose() -> dict:
    """Load docker-compose.yml once for all tests in this module."""
    with COMPOSE_PATH.open() as fh:
        return yaml.safe_load(fh)


# ---------------------------------------------------------------------------
# 1. Exactly one service runs "alembic upgrade head"
# ---------------------------------------------------------------------------

def test_exactly_one_alembic_upgrade_service(compose):
    """Exactly one service must run 'alembic upgrade head' — the migrate service."""
    alembic_services = [
        name
        for name, svc in compose["services"].items()
        if "alembic upgrade head" in " ".join(
            svc.get("command", [])
            if isinstance(svc.get("command"), list)
            else [str(svc.get("command", ""))]
        )
    ]
    assert len(alembic_services) == 1, (
        f"Expected exactly 1 service running 'alembic upgrade head', "
        f"found: {alembic_services}"
    )
    assert alembic_services[0] == "migrate", (
        f"Migration service must be named 'migrate', got: {alembic_services[0]}"
    )


# ---------------------------------------------------------------------------
# 2. The migrate service uses DATABASE_MIGRATE_URL, not DATABASE_URL
# ---------------------------------------------------------------------------

def test_migrate_service_uses_migrate_url(compose):
    """The migrate service must reference DATABASE_MIGRATE_URL."""
    svc = compose["services"]["migrate"]
    env = svc.get("environment", [])
    # environment may be a list of "KEY=VALUE" or "KEY" strings, or a dict
    if isinstance(env, dict):
        keys = set(env.keys())
    else:
        keys = {e.split("=")[0] for e in env}

    assert "DATABASE_MIGRATE_URL" in keys, (
        "migrate service must declare DATABASE_MIGRATE_URL in its environment"
    )
    assert "DATABASE_URL" not in keys, (
        "migrate service must NOT have DATABASE_URL — it uses the DDL-capable migrate role only"
    )


# ---------------------------------------------------------------------------
# 3. Application services must NOT run alembic upgrade head in their command
# ---------------------------------------------------------------------------

def test_application_services_do_not_run_alembic(compose):
    """backend/celery-worker/celery-beat must not run 'alembic upgrade' themselves."""
    for name in APPLICATION_SERVICES:
        svc = compose["services"][name]
        cmd = svc.get("command", "")
        if isinstance(cmd, list):
            cmd = " ".join(cmd)
        assert "alembic upgrade" not in cmd, (
            f"Service '{name}' must not run 'alembic upgrade' — only the migrate service should"
        )


# ---------------------------------------------------------------------------
# 4. All application services depend on migrate with service_completed_successfully
# ---------------------------------------------------------------------------

def test_application_services_depend_on_migrate(compose):
    """backend, celery-worker, and celery-beat must all wait for migrate to complete."""
    for name in APPLICATION_SERVICES:
        svc = compose["services"][name]
        depends = svc.get("depends_on", {})
        assert "migrate" in depends, (
            f"Service '{name}' is missing 'migrate' in depends_on"
        )
        condition = depends["migrate"].get("condition")
        assert condition == "service_completed_successfully", (
            f"Service '{name}' depends_on.migrate.condition must be "
            f"'service_completed_successfully', got: {condition!r}"
        )


# ---------------------------------------------------------------------------
# 5. The migrate service preserves container hardening
# ---------------------------------------------------------------------------

def test_migrate_service_has_container_hardening(compose):
    """migrate service must have read_only, cap_drop, and no-new-privileges."""
    svc = compose["services"]["migrate"]

    assert svc.get("read_only") is True, (
        "migrate service must set read_only: true"
    )

    cap_drop = svc.get("cap_drop", [])
    assert "ALL" in cap_drop, (
        "migrate service must cap_drop ALL"
    )

    sec_opts = svc.get("security_opt", [])
    assert "no-new-privileges:true" in sec_opts, (
        "migrate service must set no-new-privileges:true"
    )


# ---------------------------------------------------------------------------
# 6. The migrate service has restart: "no" (it is one-shot, not a daemon)
# ---------------------------------------------------------------------------

def test_migrate_service_is_one_shot(compose):
    """migrate service must have restart: 'no' — it exits after running migrations."""
    svc = compose["services"]["migrate"]
    restart = svc.get("restart", "")
    assert restart == "no", (
        f"migrate service restart policy must be 'no' (one-shot), got: {restart!r}"
    )


# ---------------------------------------------------------------------------
# 7. The migrate service only depends on postgres (not minio/redis)
# ---------------------------------------------------------------------------

def test_migrate_service_minimal_dependencies(compose):
    """migrate only needs postgres to be healthy — minio/redis are runtime concerns."""
    svc = compose["services"]["migrate"]
    depends = svc.get("depends_on", {})
    assert "postgres" in depends, "migrate must depend on postgres"
    assert depends["postgres"]["condition"] == "service_healthy", (
        "migrate must wait for postgres to be healthy"
    )
    for extra in ("minio", "redis", "minio-init"):
        assert extra not in depends, (
            f"migrate should NOT depend on '{extra}' — only postgres is needed for schema migrations"
        )
