"""Tests for escalation routes and schemas (escalation_routes.py)."""

import pytest
from unittest.mock import MagicMock, patch
from datetime import datetime
from fastapi.testclient import TestClient
from fastapi import FastAPI

from compliance.api.escalation_routes import (
    router,
    EscalationCreate,
    EscalationUpdate,
    EscalationStatusUpdate,
)
from compliance.api.db_utils import row_to_dict as _row_to_dict

DEFAULT_TENANT_ID = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"

from classroom_engine.database import get_db

# =============================================================================
# App + Client setup
# =============================================================================

app = FastAPI()
app.include_router(router)

ESCALATION_ID = "eeeeeeee-0001-0001-0001-000000000001"
UNKNOWN_ID = "aaaaaaaa-9999-9999-9999-999999999999"


# =============================================================================
# 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_escalation_row(overrides=None):
    now = datetime(2026, 3, 6, 12, 0, 0)
    data = {
        "id": ESCALATION_ID,
        "tenant_id": DEFAULT_TENANT_ID,
        "title": "Test Eskalation",
        "description": "Beschreibung",
        "priority": "medium",
        "status": "open",
        "category": "dsgvo_breach",
        "assignee": "admin@example.com",
        "reporter": "user@example.com",
        "source_module": None,
        "source_id": None,
        "due_date": None,
        "resolved_at": None,
        "created_at": now,
        "updated_at": now,
    }
    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)


# =============================================================================
# Schema Tests — EscalationCreate
# =============================================================================

class TestEscalationCreate:
    def test_minimal_valid(self):
        req = EscalationCreate(title="Test Eskalation")
        assert req.title == "Test Eskalation"
        assert req.priority == "medium"
        assert req.description is None
        assert req.category is None
        assert req.assignee is None

    def test_full_values(self):
        req = EscalationCreate(
            title="DSGVO-Verstoß",
            description="Datenleck entdeckt",
            priority="critical",
            category="dsgvo_breach",
            assignee="admin@example.com",
            reporter="user@example.com",
            source_module="incidents",
        )
        assert req.title == "DSGVO-Verstoß"
        assert req.priority == "critical"
        assert req.category == "dsgvo_breach"
        assert req.source_module == "incidents"

    def test_serialization(self):
        req = EscalationCreate(title="Test", priority="high")
        data = req.model_dump(exclude_none=True)
        assert data["title"] == "Test"
        assert data["priority"] == "high"
        assert "description" not in data


# =============================================================================
# Schema Tests — EscalationUpdate
# =============================================================================

class TestEscalationUpdate:
    def test_empty_update(self):
        req = EscalationUpdate()
        data = req.model_dump(exclude_none=True)
        assert data == {}

    def test_partial_update(self):
        req = EscalationUpdate(assignee="new@example.com", priority="low")
        data = req.model_dump(exclude_none=True)
        assert data == {"assignee": "new@example.com", "priority": "low"}

    def test_title_update(self):
        req = EscalationUpdate(title="Aktualisierter Titel")
        data = req.model_dump(exclude_none=True)
        assert data["title"] == "Aktualisierter Titel"
        assert "priority" not in data


# =============================================================================
# Schema Tests — EscalationStatusUpdate
# =============================================================================

class TestEscalationStatusUpdate:
    def test_status_only(self):
        req = EscalationStatusUpdate(status="in_progress")
        assert req.status == "in_progress"
        assert req.resolved_at is None

    def test_with_resolved_at(self):
        ts = datetime(2026, 3, 1, 12, 0, 0)
        req = EscalationStatusUpdate(status="resolved", resolved_at=ts)
        assert req.status == "resolved"
        assert req.resolved_at == ts

    def test_closed_status(self):
        req = EscalationStatusUpdate(status="closed")
        assert req.status == "closed"


# =============================================================================
# Helper Tests — _row_to_dict
# =============================================================================

class TestRowToDict:
    def test_basic_conversion(self):
        row = MagicMock()
        row._mapping = {"id": "abc-123", "title": "Test", "priority": "medium"}
        result = _row_to_dict(row)
        assert result["id"] == "abc-123"
        assert result["title"] == "Test"
        assert result["priority"] == "medium"

    def test_datetime_serialized(self):
        ts = datetime(2026, 3, 1, 10, 0, 0)
        row = MagicMock()
        row._mapping = {"id": "abc", "created_at": ts}
        result = _row_to_dict(row)
        assert result["created_at"] == ts.isoformat()

    def test_none_values_preserved(self):
        row = MagicMock()
        row._mapping = {"id": "abc", "description": None, "resolved_at": None}
        result = _row_to_dict(row)
        assert result["description"] is None
        assert result["resolved_at"] is None


# =============================================================================
# Route Tests — List Escalations
# =============================================================================

class TestListEscalations:
    def test_list_empty(self, client, mock_db):
        mock_db.execute.side_effect = None
        mock_db.execute.return_value = MagicMock(
            fetchall=MagicMock(return_value=[]),
            fetchone=MagicMock(return_value=_count_row(0)),
        )
        # fetchall for items, then fetchone for count
        mock_db.execute.side_effect = [
            MagicMock(fetchall=MagicMock(return_value=[])),
            MagicMock(fetchone=MagicMock(return_value=_count_row(0))),
        ]
        resp = client.get("/escalations")
        assert resp.status_code == 200
        data = resp.json()
        assert data["items"] == []
        assert data["total"] == 0

    def test_list_with_items(self, client, mock_db):
        row = _make_escalation_row()
        mock_db.execute.side_effect = [
            MagicMock(fetchall=MagicMock(return_value=[row])),
            MagicMock(fetchone=MagicMock(return_value=_count_row(1))),
        ]
        resp = client.get("/escalations")
        assert resp.status_code == 200
        data = resp.json()
        assert len(data["items"]) == 1
        assert data["items"][0]["id"] == ESCALATION_ID
        assert data["total"] == 1

    def test_list_filter_status(self, client, mock_db):
        row = _make_escalation_row({"status": "in_progress"})
        mock_db.execute.side_effect = [
            MagicMock(fetchall=MagicMock(return_value=[row])),
            MagicMock(fetchone=MagicMock(return_value=_count_row(1))),
        ]
        resp = client.get("/escalations?status=in_progress")
        assert resp.status_code == 200
        data = resp.json()
        assert len(data["items"]) == 1

    def test_list_filter_priority(self, client, mock_db):
        row = _make_escalation_row({"priority": "critical"})
        mock_db.execute.side_effect = [
            MagicMock(fetchall=MagicMock(return_value=[row])),
            MagicMock(fetchone=MagicMock(return_value=_count_row(1))),
        ]
        resp = client.get("/escalations?priority=critical")
        assert resp.status_code == 200
        assert resp.json()["items"][0]["priority"] == "critical"


# =============================================================================
# Route Tests — Create Escalation
# =============================================================================

class TestCreateEscalation:
    def test_create_basic(self, client, mock_db):
        row = _make_escalation_row()
        mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=row))
        resp = client.post("/escalations", json={"title": "Test Eskalation"})
        assert resp.status_code == 201
        data = resp.json()
        assert data["id"] == ESCALATION_ID
        assert data["title"] == "Test Eskalation"
        mock_db.commit.assert_called_once()

    def test_create_full_fields(self, client, mock_db):
        row = _make_escalation_row({
            "priority": "critical",
            "category": "ai_act",
            "assignee": "dsb@example.com",
        })
        mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=row))
        resp = client.post("/escalations", json={
            "title": "AI Act Verstoß",
            "description": "Hochrisiko-KI ohne Dokumentation",
            "priority": "critical",
            "category": "ai_act",
            "assignee": "dsb@example.com",
        })
        assert resp.status_code == 201
        assert resp.json()["priority"] == "critical"

    def test_create_missing_title_422(self, client, mock_db):
        resp = client.post("/escalations", json={"description": "Ohne Titel"})
        assert resp.status_code == 422


# =============================================================================
# Route Tests — Get Escalation
# =============================================================================

class TestGetEscalation:
    def test_get_existing(self, client, mock_db):
        row = _make_escalation_row()
        mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=row))
        resp = client.get(f"/escalations/{ESCALATION_ID}")
        assert resp.status_code == 200
        assert resp.json()["id"] == ESCALATION_ID

    def test_get_not_found(self, client, mock_db):
        mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=None))
        resp = client.get(f"/escalations/{UNKNOWN_ID}")
        assert resp.status_code == 404


# =============================================================================
# Route Tests — Update Escalation
# =============================================================================

class TestUpdateEscalation:
    def test_update_partial(self, client, mock_db):
        existing = _make_escalation_row()
        updated = _make_escalation_row({"priority": "high"})
        mock_db.execute.side_effect = [
            MagicMock(fetchone=MagicMock(return_value=existing)),  # existence check
            MagicMock(fetchone=MagicMock(return_value=updated)),   # update RETURNING
        ]
        resp = client.put(f"/escalations/{ESCALATION_ID}", json={"priority": "high"})
        assert resp.status_code == 200
        assert resp.json()["priority"] == "high"
        mock_db.commit.assert_called_once()

    def test_update_not_found(self, client, mock_db):
        mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=None))
        resp = client.put(f"/escalations/{UNKNOWN_ID}", json={"title": "New"})
        assert resp.status_code == 404

    def test_update_empty_no_op(self, client, mock_db):
        row = _make_escalation_row()
        mock_db.execute.side_effect = [
            MagicMock(fetchone=MagicMock(return_value=row)),  # existence check
            MagicMock(fetchone=MagicMock(return_value=row)),  # re-fetch unchanged
        ]
        resp = client.put(f"/escalations/{ESCALATION_ID}", json={})
        assert resp.status_code == 200
        assert resp.json()["id"] == ESCALATION_ID


# =============================================================================
# Route Tests — Status Update
# =============================================================================

class TestUpdateStatus:
    def test_to_in_progress(self, client, mock_db):
        existing = _make_escalation_row()
        updated = _make_escalation_row({"status": "in_progress"})
        mock_db.execute.side_effect = [
            MagicMock(fetchone=MagicMock(return_value=existing)),
            MagicMock(fetchone=MagicMock(return_value=updated)),
        ]
        resp = client.put(f"/escalations/{ESCALATION_ID}/status", json={"status": "in_progress"})
        assert resp.status_code == 200
        assert resp.json()["status"] == "in_progress"
        mock_db.commit.assert_called_once()

    def test_to_resolved_auto_resolved_at(self, client, mock_db):
        existing = _make_escalation_row()
        now = datetime(2026, 3, 6, 14, 0, 0)
        updated = _make_escalation_row({"status": "resolved", "resolved_at": now})
        mock_db.execute.side_effect = [
            MagicMock(fetchone=MagicMock(return_value=existing)),
            MagicMock(fetchone=MagicMock(return_value=updated)),
        ]
        resp = client.put(f"/escalations/{ESCALATION_ID}/status", json={"status": "resolved"})
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "resolved"
        assert data["resolved_at"] is not None

    def test_to_closed(self, client, mock_db):
        existing = _make_escalation_row()
        updated = _make_escalation_row({"status": "closed"})
        mock_db.execute.side_effect = [
            MagicMock(fetchone=MagicMock(return_value=existing)),
            MagicMock(fetchone=MagicMock(return_value=updated)),
        ]
        resp = client.put(f"/escalations/{ESCALATION_ID}/status", json={"status": "closed"})
        assert resp.status_code == 200
        assert resp.json()["status"] == "closed"

    def test_status_not_found(self, client, mock_db):
        mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=None))
        resp = client.put(f"/escalations/{UNKNOWN_ID}/status", json={"status": "closed"})
        assert resp.status_code == 404


# =============================================================================
# Route Tests — Delete Escalation
# =============================================================================

class TestDeleteEscalation:
    def test_delete_existing(self, client, mock_db):
        row = _make_escalation_row()
        mock_db.execute.side_effect = [
            MagicMock(fetchone=MagicMock(return_value=row)),  # existence check
            None,  # DELETE
        ]
        resp = client.delete(f"/escalations/{ESCALATION_ID}")
        assert resp.status_code == 200
        assert resp.json()["success"] is True
        mock_db.commit.assert_called_once()

    def test_delete_not_found(self, client, mock_db):
        mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=None))
        resp = client.delete(f"/escalations/{UNKNOWN_ID}")
        assert resp.status_code == 404


# =============================================================================
# Route Tests — Stats
# =============================================================================

class TestGetStats:
    def test_stats_empty(self, client, mock_db):
        mock_db.execute.side_effect = [
            MagicMock(fetchall=MagicMock(return_value=[])),      # by_status
            MagicMock(fetchall=MagicMock(return_value=[])),      # by_priority
            MagicMock(fetchone=MagicMock(return_value=_count_row(0))),  # total
            MagicMock(fetchone=MagicMock(return_value=_count_row(0))),  # active
        ]
        resp = client.get("/escalations/stats")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 0
        assert data["active"] == 0
        assert data["by_status"]["open"] == 0

    def test_stats_with_data(self, client, mock_db):
        status_rows = [_MockRow({"0": "open", "1": 3}), _MockRow({"0": "resolved", "1": 1})]
        # These rows need [0] and [1] indexing
        sr1 = MagicMock(); sr1.__getitem__ = lambda s, i: ["open", 3][i]
        sr2 = MagicMock(); sr2.__getitem__ = lambda s, i: ["resolved", 1][i]
        pr1 = MagicMock(); pr1.__getitem__ = lambda s, i: ["high", 2][i]
        pr2 = MagicMock(); pr2.__getitem__ = lambda s, i: ["medium", 2][i]

        mock_db.execute.side_effect = [
            MagicMock(fetchall=MagicMock(return_value=[sr1, sr2])),
            MagicMock(fetchall=MagicMock(return_value=[pr1, pr2])),
            MagicMock(fetchone=MagicMock(return_value=_count_row(4))),
            MagicMock(fetchone=MagicMock(return_value=_count_row(3))),
        ]
        resp = client.get("/escalations/stats")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 4
        assert data["active"] == 3
        assert data["by_status"]["open"] == 3
        assert data["by_status"]["resolved"] == 1

    def test_stats_structure(self, client, mock_db):
        mock_db.execute.side_effect = [
            MagicMock(fetchall=MagicMock(return_value=[])),
            MagicMock(fetchall=MagicMock(return_value=[])),
            MagicMock(fetchone=MagicMock(return_value=_count_row(0))),
            MagicMock(fetchone=MagicMock(return_value=_count_row(0))),
        ]
        resp = client.get("/escalations/stats")
        data = resp.json()
        assert set(data["by_status"].keys()) == {"open", "in_progress", "escalated", "resolved", "closed"}
        assert set(data["by_priority"].keys()) == {"low", "medium", "high", "critical"}
        assert "total" in data
        assert "active" in data
