"""Tests for edge agent security hardening: auth, container hardening, crash recovery."""

from __future__ import annotations

import os
from unittest.mock import patch, MagicMock

import pytest
from fastapi.testclient import TestClient

AUTH_TOKEN = "test-secret-token-12345"


# ============================================================
# Fixtures
# ============================================================


@pytest.fixture
def auth_client(db_path, monkeypatch):
    """TestClient with a specific auth token configured."""
    monkeypatch.setenv("RADIX_AUTH_TOKEN", AUTH_TOKEN)
    from radix_edge.config import reset_config
    reset_config()
    from radix_edge.api.app import create_app
    app = create_app()
    return TestClient(app)


@pytest.fixture
def no_auth_client(db_path, monkeypatch):
    """TestClient with NO auth token configured (empty string)."""
    monkeypatch.setenv("RADIX_AUTH_TOKEN", "")
    from radix_edge.config import reset_config
    reset_config()
    from radix_edge.api.app import create_app
    app = create_app()
    return TestClient(app)


@pytest.fixture
def auth_headers():
    return {"Authorization": f"Bearer {AUTH_TOKEN}"}


# ============================================================
# FEATURE 1: API Authentication
# ============================================================


class TestAuth:
    def test_health_endpoints_no_auth(self, auth_client):
        """Health endpoints should be accessible without authentication."""
        resp = auth_client.get("/healthz")
        assert resp.status_code == 200
        assert resp.json()["status"] == "healthy"

        resp = auth_client.get("/readyz")
        assert resp.status_code == 200

    def test_jobs_requires_auth(self, auth_client):
        """GET /v1/jobs without auth returns 401."""
        resp = auth_client.get("/v1/jobs")
        assert resp.status_code == 401

    def test_jobs_with_valid_auth(self, auth_client, auth_headers):
        """GET /v1/jobs with valid Bearer token returns 200."""
        resp = auth_client.get("/v1/jobs", headers=auth_headers)
        assert resp.status_code == 200

    def test_jobs_with_invalid_auth(self, auth_client):
        """GET /v1/jobs with wrong token returns 401."""
        resp = auth_client.get("/v1/jobs", headers={"Authorization": "Bearer wrong-token"})
        assert resp.status_code == 401

    def test_gpus_requires_auth(self, auth_client):
        """GET /v1/gpus without auth returns 401."""
        resp = auth_client.get("/v1/gpus")
        assert resp.status_code == 401

    def test_admin_requires_auth(self, auth_client):
        """GET /v1/config/brain without auth returns 401."""
        resp = auth_client.get("/v1/config/brain")
        assert resp.status_code == 401

    def test_submit_job_requires_auth(self, auth_client):
        """POST /v1/jobs without auth returns 401."""
        resp = auth_client.post("/v1/jobs", json={"image": "test:latest", "user_id": "u1"})
        assert resp.status_code == 401

    def test_no_auth_token_allows_local_access(self, no_auth_client):
        """When RADIX_AUTH_TOKEN is empty, requests are allowed (local mode)."""
        resp = no_auth_client.get(
            "/v1/jobs",
            headers={"Authorization": "Bearer some-token"},
        )
        assert resp.status_code == 200


# ============================================================
# FEATURE 2: Container Hardening
# ============================================================


class TestContainerHardening:
    def _build_cmd(self, monkeypatch, **overrides):
        """Helper to build a docker command with default args."""
        monkeypatch.setenv("RADIX_AUTH_TOKEN", "x")
        from radix_edge.config import reset_config
        reset_config()
        from radix_edge.executor.executor import _build_docker_command

        defaults = dict(
            image="python:3.11",
            command=["python", "train.py"],
            args=None,
            env=None,
            gpu_indices=[0],
            num_gpus=1,
            job_dir="/tmp/test-job",
            job_id="job-test123",
            timeout_seconds=3600,
        )
        defaults.update(overrides)
        return _build_docker_command(**defaults)

    def test_docker_command_has_user_flag(self, monkeypatch):
        cmd = self._build_cmd(monkeypatch)
        assert "--user" in cmd
        idx = cmd.index("--user")
        assert cmd[idx + 1] == "1000:1000"

    def test_docker_command_drops_caps(self, monkeypatch):
        cmd = self._build_cmd(monkeypatch)
        assert "--cap-drop=ALL" in cmd

    def test_docker_command_read_only(self, monkeypatch):
        cmd = self._build_cmd(monkeypatch)
        assert "--read-only" in cmd

    def test_docker_command_no_new_privileges(self, monkeypatch):
        cmd = self._build_cmd(monkeypatch)
        assert "--security-opt=no-new-privileges" in cmd

    def test_docker_command_memory_limit(self, monkeypatch):
        cmd = self._build_cmd(monkeypatch)
        assert "--memory=16g" in cmd

    def test_docker_command_cpu_limit(self, monkeypatch):
        cmd = self._build_cmd(monkeypatch)
        assert "--cpus=4" in cmd

    def test_docker_command_pids_limit(self, monkeypatch):
        cmd = self._build_cmd(monkeypatch)
        assert "--pids-limit=4096" in cmd

    def test_docker_command_network_none(self, monkeypatch):
        cmd = self._build_cmd(monkeypatch)
        assert "--network=none" in cmd

    def test_docker_command_has_labels(self, monkeypatch):
        cmd = self._build_cmd(monkeypatch, job_id="job-abc123")
        assert "radix.managed=true" in cmd
        assert "radix.job_id=job-abc123" in cmd

    def test_docker_command_no_socket_mount(self, monkeypatch):
        cmd = self._build_cmd(monkeypatch)
        full_cmd = " ".join(cmd)
        assert "/var/run/docker.sock" not in full_cmd

    def test_image_validation_allows_matching(self, monkeypatch):
        monkeypatch.setenv("RADIX_AUTH_TOKEN", "x")
        monkeypatch.setenv("RADIX_ALLOWED_IMAGE_PATTERNS", r"^nvidia/.*,^python:.*")
        from radix_edge.config import reset_config
        reset_config()
        from radix_edge.executor.executor import validate_image

        # Should not raise
        validate_image("nvidia/cuda:12.4")
        validate_image("python:3.11")

    def test_image_validation_rejects_non_matching(self, monkeypatch):
        monkeypatch.setenv("RADIX_AUTH_TOKEN", "x")
        monkeypatch.setenv("RADIX_ALLOWED_IMAGE_PATTERNS", r"^nvidia/.*,^python:.*")
        from radix_edge.config import reset_config
        reset_config()
        from radix_edge.executor.executor import validate_image

        with pytest.raises(ValueError, match="does not match"):
            validate_image("malicious/image:latest")

    def test_image_validation_empty_allowlist_permits_all(self, monkeypatch):
        monkeypatch.setenv("RADIX_AUTH_TOKEN", "x")
        monkeypatch.setenv("RADIX_ALLOWED_IMAGE_PATTERNS", "")
        from radix_edge.config import reset_config
        reset_config()
        from radix_edge.executor.executor import validate_image

        # Should not raise for any image
        validate_image("anything/goes:latest")
        validate_image("totally-random-image")

    def test_schema_image_max_length(self):
        from radix_edge.api.schemas import JobSubmitRequest
        from pydantic import ValidationError

        with pytest.raises(ValidationError):
            JobSubmitRequest(image="x" * 513)

    def test_schema_name_max_length(self):
        from radix_edge.api.schemas import JobSubmitRequest
        from pydantic import ValidationError

        with pytest.raises(ValidationError):
            JobSubmitRequest(image="valid:img", name="x" * 256)


# ============================================================
# FEATURE 3: Crash Recovery
# ============================================================


class TestCrashRecovery:
    def test_reconcile_no_orphans(self, db):
        """Clean state should return zero counts."""
        with patch("radix_edge.recovery.subprocess") as mock_subprocess:
            mock_result = MagicMock()
            mock_result.returncode = 0
            mock_result.stdout = ""
            mock_subprocess.run.return_value = mock_result

            from radix_edge.recovery import reconcile_on_startup
            summary = reconcile_on_startup()

        assert summary["orphaned_containers_killed"] == 0
        assert summary["stale_jobs_failed"] == 0
        assert summary["stale_gpu_allocations_released"] == 0

    def test_reconcile_marks_stale_jobs_failed(self, db):
        """Jobs in running state with no Docker container are marked failed."""
        db.execute(
            "INSERT INTO gpus (gpu_index, name, memory_total_mb, uuid) VALUES (0, 'A10G', 23028, 'GPU-test')"
        )
        db.execute(
            "INSERT INTO jobs (job_id, user_id, image, status) VALUES (?, ?, ?, ?)",
            ("job-orphan", "user1", "python:3.11", "running"),
        )
        db.commit()

        with patch("radix_edge.recovery.subprocess") as mock_subprocess:
            mock_result = MagicMock()
            mock_result.returncode = 0
            mock_result.stdout = ""  # No containers running
            mock_subprocess.run.return_value = mock_result

            from radix_edge.recovery import reconcile_on_startup
            summary = reconcile_on_startup()

        assert summary["stale_jobs_failed"] == 1

        row = db.execute("SELECT status, error_message FROM jobs WHERE job_id = 'job-orphan'").fetchone()
        assert row["status"] == "failed"
        assert "restarted" in row["error_message"]

    def test_reconcile_releases_stale_gpus(self, db):
        """GPUs allocated to completed/failed jobs are released."""
        db.execute(
            "INSERT INTO gpus (gpu_index, name, memory_total_mb, uuid, status, assigned_job_id) "
            "VALUES (0, 'A10G', 23028, 'GPU-test', 'allocated', 'job-done')"
        )
        db.execute(
            "INSERT INTO jobs (job_id, user_id, image, status) VALUES (?, ?, ?, ?)",
            ("job-done", "user1", "python:3.11", "succeeded"),
        )
        db.commit()

        with patch("radix_edge.recovery.subprocess") as mock_subprocess:
            mock_result = MagicMock()
            mock_result.returncode = 0
            mock_result.stdout = ""
            mock_subprocess.run.return_value = mock_result

            from radix_edge.recovery import reconcile_on_startup
            summary = reconcile_on_startup()

        assert summary["stale_gpu_allocations_released"] == 1

        row = db.execute("SELECT status, assigned_job_id FROM gpus WHERE gpu_index = 0").fetchone()
        assert row["status"] == "idle"
        assert row["assigned_job_id"] is None
