"""Tests for compliance process task routes (process_task_routes.py)."""

import pytest
from unittest.mock import MagicMock, patch, call
from datetime import datetime, date, timedelta

from fastapi import FastAPI
from fastapi.testclient import TestClient

from compliance.api.process_task_routes import (
    router,
    ProcessTaskCreate,
    ProcessTaskUpdate,
    ProcessTaskComplete,
    ProcessTaskSkip,
    VALID_CATEGORIES,
    VALID_FREQUENCIES,
    VALID_PRIORITIES,
    VALID_STATUSES,
    FREQUENCY_DAYS,
)
from classroom_engine.database import get_db
from compliance.api.tenant_utils import get_tenant_id

DEFAULT_TENANT_ID = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"
TASK_ID = "ffffffff-0001-0001-0001-000000000001"

app = FastAPI()
app.include_router(router)


# ---------------------------------------------------------------------------
# Mock helpers
# ---------------------------------------------------------------------------

class _MockRow:
    """Simulates a SQLAlchemy row with _mapping attribute."""
    def __init__(self, data: dict):
        self._mapping = data

    def __getitem__(self, idx):
        vals = list(self._mapping.values())
        return vals[idx]


def _make_task_row(overrides=None):
    now = datetime(2026, 3, 14, 12, 0, 0)
    data = {
        "id": TASK_ID,
        "tenant_id": DEFAULT_TENANT_ID,
        "project_id": None,
        "task_code": "DSGVO-VVT-REVIEW",
        "title": "VVT-Review und Aktualisierung",
        "description": "Jaehrliche Ueberpruefung des VVT.",
        "category": "dsgvo",
        "priority": "high",
        "frequency": "yearly",
        "assigned_to": None,
        "responsible_team": None,
        "linked_control_ids": [],
        "linked_module": "vvt",
        "last_completed_at": None,
        "next_due_date": date(2027, 3, 14),
        "due_reminder_days": 14,
        "status": "pending",
        "completion_date": None,
        "completion_result": None,
        "completion_evidence_id": None,
        "follow_up_actions": [],
        "is_seed": False,
        "notes": None,
        "tags": [],
        "created_at": now,
        "updated_at": now,
    }
    if overrides:
        data.update(overrides)
    return _MockRow(data)


def _make_history_row(overrides=None):
    now = datetime(2026, 3, 14, 12, 0, 0)
    data = {
        "id": "eeeeeeee-0001-0001-0001-000000000001",
        "task_id": TASK_ID,
        "completed_by": "admin",
        "completed_at": now,
        "result": "Alles in Ordnung",
        "evidence_id": None,
        "notes": "Keine Auffaelligkeiten",
        "status": "completed",
    }
    if overrides:
        data.update(overrides)
    return _MockRow(data)


def _count_row(val):
    """Simulates a COUNT(*) row — fetchone()[0] returns the value."""
    row = MagicMock()
    row.__getitem__ = lambda self, idx: val
    return row


@pytest.fixture
def mock_db():
    db = MagicMock()
    app.dependency_overrides[get_db] = lambda: db
    yield db
    app.dependency_overrides.pop(get_db, None)


@pytest.fixture
def client(mock_db):
    return TestClient(app)


# =============================================================================
# Test 1: List Tasks
# =============================================================================

class TestListTasks:
    def test_list_tasks(self, client, mock_db):
        """List tasks returns items and total."""
        row = _make_task_row()
        mock_db.execute.side_effect = [
            MagicMock(fetchone=MagicMock(return_value=_count_row(1))),
            MagicMock(fetchall=MagicMock(return_value=[row])),
        ]
        resp = client.get("/process-tasks")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 1
        assert len(data["tasks"]) == 1
        assert data["tasks"][0]["id"] == TASK_ID
        assert data["tasks"][0]["task_code"] == "DSGVO-VVT-REVIEW"

    def test_list_tasks_empty(self, client, mock_db):
        """List tasks returns empty when no tasks."""
        mock_db.execute.side_effect = [
            MagicMock(fetchone=MagicMock(return_value=_count_row(0))),
            MagicMock(fetchall=MagicMock(return_value=[])),
        ]
        resp = client.get("/process-tasks")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 0
        assert data["tasks"] == []


# =============================================================================
# Test 2: List Tasks with Filters
# =============================================================================

class TestListTasksWithFilters:
    def test_list_tasks_with_filters(self, client, mock_db):
        """Filter by status and category."""
        row = _make_task_row({"status": "overdue", "category": "nis2"})
        mock_db.execute.side_effect = [
            MagicMock(fetchone=MagicMock(return_value=_count_row(1))),
            MagicMock(fetchall=MagicMock(return_value=[row])),
        ]
        resp = client.get("/process-tasks?status=overdue&category=nis2")
        assert resp.status_code == 200
        data = resp.json()
        assert data["tasks"][0]["status"] == "overdue"
        assert data["tasks"][0]["category"] == "nis2"

    def test_list_tasks_overdue_filter(self, client, mock_db):
        """Filter overdue=true adds date condition."""
        mock_db.execute.side_effect = [
            MagicMock(fetchone=MagicMock(return_value=_count_row(0))),
            MagicMock(fetchall=MagicMock(return_value=[])),
        ]
        resp = client.get("/process-tasks?overdue=true")
        assert resp.status_code == 200
        # Verify the SQL was called (mock_db.execute called twice: count + select)
        assert mock_db.execute.call_count == 2


# =============================================================================
# Test 3: Get Stats
# =============================================================================

class TestGetStats:
    def test_get_stats(self, client, mock_db):
        """Verify stat counts structure."""
        stats_row = MagicMock()
        stats_row._mapping = {
            "total": 50,
            "pending": 20,
            "in_progress": 5,
            "completed": 15,
            "overdue": 8,
            "skipped": 2,
            "overdue_count": 8,
            "due_7_days": 3,
            "due_14_days": 7,
            "due_30_days": 12,
        }
        cat_row1 = MagicMock()
        cat_row1._mapping = {"category": "dsgvo", "cnt": 15}
        cat_row2 = MagicMock()
        cat_row2._mapping = {"category": "nis2", "cnt": 10}

        mock_db.execute.side_effect = [
            MagicMock(fetchone=MagicMock(return_value=stats_row)),
            MagicMock(fetchall=MagicMock(return_value=[cat_row1, cat_row2])),
        ]
        resp = client.get("/process-tasks/stats")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 50
        assert data["by_status"]["pending"] == 20
        assert data["by_status"]["completed"] == 15
        assert data["overdue_count"] == 8
        assert data["due_7_days"] == 3
        assert data["due_30_days"] == 12
        assert data["by_category"]["dsgvo"] == 15
        assert data["by_category"]["nis2"] == 10

    def test_get_stats_empty(self, client, mock_db):
        """Stats with no tasks returns zeros."""
        mock_db.execute.side_effect = [
            MagicMock(fetchone=MagicMock(return_value=None)),
            MagicMock(fetchall=MagicMock(return_value=[])),
        ]
        resp = client.get("/process-tasks/stats")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 0
        assert data["by_category"] == {}


# =============================================================================
# Test 4: Create Task
# =============================================================================

class TestCreateTask:
    def test_create_task(self, client, mock_db):
        """Create a valid task returns 201."""
        row = _make_task_row()
        mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=row))
        resp = client.post("/process-tasks", json={
            "task_code": "DSGVO-VVT-REVIEW",
            "title": "VVT-Review und Aktualisierung",
            "category": "dsgvo",
            "priority": "high",
            "frequency": "yearly",
        })
        assert resp.status_code == 201
        data = resp.json()
        assert data["id"] == TASK_ID
        assert data["task_code"] == "DSGVO-VVT-REVIEW"
        mock_db.commit.assert_called_once()


# =============================================================================
# Test 5: Create Task Invalid Category
# =============================================================================

class TestCreateTaskInvalidCategory:
    def test_create_task_invalid_category(self, client, mock_db):
        """Invalid category returns 400."""
        resp = client.post("/process-tasks", json={
            "task_code": "TEST-001",
            "title": "Test",
            "category": "invalid_category",
        })
        assert resp.status_code == 400
        assert "Invalid category" in resp.json()["detail"]

    def test_create_task_invalid_priority(self, client, mock_db):
        """Invalid priority returns 400."""
        resp = client.post("/process-tasks", json={
            "task_code": "TEST-001",
            "title": "Test",
            "category": "dsgvo",
            "priority": "super_high",
        })
        assert resp.status_code == 400
        assert "Invalid priority" in resp.json()["detail"]

    def test_create_task_invalid_frequency(self, client, mock_db):
        """Invalid frequency returns 400."""
        resp = client.post("/process-tasks", json={
            "task_code": "TEST-001",
            "title": "Test",
            "category": "dsgvo",
            "frequency": "biweekly",
        })
        assert resp.status_code == 400
        assert "Invalid frequency" in resp.json()["detail"]


# =============================================================================
# Test 6: Get Single Task
# =============================================================================

class TestGetSingleTask:
    def test_get_single_task(self, client, mock_db):
        """Get existing task by ID."""
        row = _make_task_row()
        mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=row))
        resp = client.get(f"/process-tasks/{TASK_ID}")
        assert resp.status_code == 200
        assert resp.json()["id"] == TASK_ID

    def test_get_task_not_found(self, client, mock_db):
        """Get non-existent task returns 404."""
        mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=None))
        resp = client.get("/process-tasks/nonexistent-id")
        assert resp.status_code == 404


# =============================================================================
# Test 7: Complete Task
# =============================================================================

class TestCompleteTask:
    def test_complete_task(self, client, mock_db):
        """Complete a task: verify history insert and next_due recalculation."""
        task_row = _make_task_row({"frequency": "quarterly"})
        updated_row = _make_task_row({
            "frequency": "quarterly",
            "status": "pending",
            "last_completed_at": datetime(2026, 3, 14, 12, 0, 0),
            "next_due_date": date(2026, 6, 12),
        })

        # First call: SELECT task, Second: INSERT history, Third: UPDATE task
        mock_db.execute.side_effect = [
            MagicMock(fetchone=MagicMock(return_value=task_row)),
            MagicMock(),  # history INSERT
            MagicMock(fetchone=MagicMock(return_value=updated_row)),
        ]

        resp = client.post(f"/process-tasks/{TASK_ID}/complete", json={
            "completed_by": "admin",
            "result": "Alles geprueft",
            "notes": "Keine Auffaelligkeiten",
        })
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "pending"  # Reset for recurring
        mock_db.commit.assert_called_once()

    def test_complete_once_task(self, client, mock_db):
        """Complete a one-time task stays completed."""
        task_row = _make_task_row({"frequency": "once"})
        updated_row = _make_task_row({
            "frequency": "once",
            "status": "completed",
            "next_due_date": None,
        })

        mock_db.execute.side_effect = [
            MagicMock(fetchone=MagicMock(return_value=task_row)),
            MagicMock(),
            MagicMock(fetchone=MagicMock(return_value=updated_row)),
        ]

        resp = client.post(f"/process-tasks/{TASK_ID}/complete", json={
            "completed_by": "admin",
        })
        assert resp.status_code == 200
        assert resp.json()["status"] == "completed"

    def test_complete_task_not_found(self, client, mock_db):
        """Complete non-existent task returns 404."""
        mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=None))
        resp = client.post("/process-tasks/nonexistent-id/complete", json={
            "completed_by": "admin",
        })
        assert resp.status_code == 404


# =============================================================================
# Test 8: Skip Task
# =============================================================================

class TestSkipTask:
    def test_skip_task(self, client, mock_db):
        """Skip task with reason, verify next_due recalculation."""
        task_row = _make_task_row({"frequency": "monthly"})
        updated_row = _make_task_row({
            "frequency": "monthly",
            "status": "pending",
            "next_due_date": date(2026, 4, 13),
        })

        mock_db.execute.side_effect = [
            MagicMock(fetchone=MagicMock(return_value=task_row)),
            MagicMock(),  # history INSERT
            MagicMock(fetchone=MagicMock(return_value=updated_row)),
        ]

        resp = client.post(f"/process-tasks/{TASK_ID}/skip", json={
            "reason": "Kein Handlungsbedarf diesen Monat",
        })
        assert resp.status_code == 200
        assert resp.json()["status"] == "pending"
        mock_db.commit.assert_called_once()

    def test_skip_task_not_found(self, client, mock_db):
        """Skip non-existent task returns 404."""
        mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=None))
        resp = client.post("/process-tasks/nonexistent-id/skip", json={
            "reason": "Test",
        })
        assert resp.status_code == 404


# =============================================================================
# Test 9: Seed Idempotent
# =============================================================================

class TestSeedIdempotent:
    def test_seed_idempotent(self, client, mock_db):
        """Seed twice — ON CONFLICT ensures idempotency."""
        # First seed: all inserted (rowcount=1 for each)
        mock_result = MagicMock()
        mock_result.rowcount = 1
        mock_db.execute.return_value = mock_result

        resp = client.post("/process-tasks/seed")
        assert resp.status_code == 200
        data = resp.json()
        assert data["seeded"] == data["total_available"]
        assert data["total_available"] == 50
        mock_db.commit.assert_called_once()

    def test_seed_second_time_no_inserts(self, client, mock_db):
        """Second seed inserts nothing (ON CONFLICT DO NOTHING)."""
        mock_result = MagicMock()
        mock_result.rowcount = 0
        mock_db.execute.return_value = mock_result

        resp = client.post("/process-tasks/seed")
        assert resp.status_code == 200
        data = resp.json()
        assert data["seeded"] == 0
        assert data["total_available"] == 50


# =============================================================================
# Test 10: Get History
# =============================================================================

class TestGetHistory:
    def test_get_history(self, client, mock_db):
        """Return history entries for a task."""
        task_id_row = _MockRow({"id": TASK_ID})
        history_row = _make_history_row()

        mock_db.execute.side_effect = [
            MagicMock(fetchone=MagicMock(return_value=task_id_row)),
            MagicMock(fetchall=MagicMock(return_value=[history_row])),
        ]

        resp = client.get(f"/process-tasks/{TASK_ID}/history")
        assert resp.status_code == 200
        data = resp.json()
        assert len(data["history"]) == 1
        assert data["history"][0]["task_id"] == TASK_ID
        assert data["history"][0]["status"] == "completed"
        assert data["history"][0]["completed_by"] == "admin"

    def test_get_history_task_not_found(self, client, mock_db):
        """History for non-existent task returns 404."""
        mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=None))
        resp = client.get("/process-tasks/nonexistent-id/history")
        assert resp.status_code == 404

    def test_get_history_empty(self, client, mock_db):
        """Task with no history returns empty list."""
        task_id_row = _MockRow({"id": TASK_ID})

        mock_db.execute.side_effect = [
            MagicMock(fetchone=MagicMock(return_value=task_id_row)),
            MagicMock(fetchall=MagicMock(return_value=[])),
        ]

        resp = client.get(f"/process-tasks/{TASK_ID}/history")
        assert resp.status_code == 200
        assert resp.json()["history"] == []


# =============================================================================
# Constant / Schema Validation Tests
# =============================================================================

class TestConstants:
    def test_valid_categories(self):
        assert VALID_CATEGORIES == {"dsgvo", "nis2", "bsi", "iso27001", "ai_act", "internal"}

    def test_valid_frequencies(self):
        assert VALID_FREQUENCIES == {"weekly", "monthly", "quarterly", "semi_annual", "yearly", "once"}

    def test_valid_priorities(self):
        assert VALID_PRIORITIES == {"critical", "high", "medium", "low"}

    def test_valid_statuses(self):
        assert VALID_STATUSES == {"pending", "in_progress", "completed", "overdue", "skipped"}

    def test_frequency_days_mapping(self):
        assert FREQUENCY_DAYS["weekly"] == 7
        assert FREQUENCY_DAYS["monthly"] == 30
        assert FREQUENCY_DAYS["quarterly"] == 90
        assert FREQUENCY_DAYS["semi_annual"] == 182
        assert FREQUENCY_DAYS["yearly"] == 365
        assert FREQUENCY_DAYS["once"] is None


class TestDeleteTask:
    def test_delete_existing(self, client, mock_db):
        mock_db.execute.return_value = MagicMock(rowcount=1)
        resp = client.delete(f"/process-tasks/{TASK_ID}")
        assert resp.status_code == 204
        mock_db.commit.assert_called_once()

    def test_delete_not_found(self, client, mock_db):
        mock_db.execute.return_value = MagicMock(rowcount=0)
        resp = client.delete(f"/process-tasks/{TASK_ID}")
        assert resp.status_code == 404
