"""Tests for Incident routes (incident_routes.py) — Datenpannen-Management DSGVO Art. 33/34."""

import json
import pytest
from unittest.mock import MagicMock
from fastapi.testclient import TestClient
from fastapi import FastAPI
from datetime import datetime, timedelta, timezone

from compliance.api.incident_routes import (
    router,
    _calculate_risk_level,
    _is_notification_required,
    _calculate_72h_deadline,
    _incident_to_response,
    _measure_to_response,
    _parse_jsonb,
    DEFAULT_TENANT_ID,
)

app = FastAPI()
app.include_router(router)
client = TestClient(app)

DEFAULT_TENANT = DEFAULT_TENANT_ID
INCIDENT_ID = "ffffffff-0001-0001-0001-000000000001"
MEASURE_ID = "ffffffff-0002-0002-0002-000000000002"
UNKNOWN_ID = "aaaaaaaa-9999-9999-9999-999999999999"


# =============================================================================
# Helpers — _DictResult / _DictSession pattern for SQLite-free DB mocks
# =============================================================================

class _DictRow:
    """Simulates a SQLAlchemy RowMapping (dict-like)."""
    def __init__(self, data: dict):
        self._data = data

    def __getitem__(self, key):
        return self._data[key]

    def __contains__(self, key):
        return key in self._data

    def keys(self):
        return self._data.keys()

    def items(self):
        return self._data.items()

    def values(self):
        return self._data.values()


class _DictResult:
    """Simulates a SQLAlchemy CursorResult."""
    def __init__(self, rows=None, scalar_val=None):
        self._rows = rows or []
        self._scalar_val = scalar_val

    def mappings(self):
        return self

    def first(self):
        return _DictRow(self._rows[0]) if self._rows else None

    def all(self):
        return [_DictRow(r) for r in self._rows]

    def scalar(self):
        return self._scalar_val


def make_incident_row(overrides=None):
    now = datetime(2024, 6, 1, 12, 0, 0)
    deadline = now + timedelta(hours=72)
    data = {
        "id": INCIDENT_ID,
        "tenant_id": DEFAULT_TENANT,
        "title": "Datenpanne Test",
        "description": "Test Beschreibung",
        "category": "data_breach",
        "status": "detected",
        "severity": "medium",
        "detected_at": now,
        "reported_by": "admin",
        "affected_data_categories": json.dumps(["personal"]),
        "affected_data_subject_count": 100,
        "affected_systems": json.dumps(["CRM"]),
        "risk_assessment": None,
        "authority_notification": json.dumps({
            "status": "pending",
            "deadline": deadline.isoformat(),
        }),
        "data_subject_notification": json.dumps({
            "required": False,
            "status": "not_required",
        }),
        "timeline": json.dumps([{
            "timestamp": now.isoformat(),
            "action": "incident_created",
            "user_id": "admin",
            "details": "Incident detected and reported",
        }]),
        "root_cause": None,
        "lessons_learned": None,
        "closed_at": None,
        "created_at": now,
        "updated_at": now,
    }
    if overrides:
        data.update(overrides)
    return data


def make_measure_row(overrides=None):
    now = datetime(2024, 6, 1, 12, 0, 0)
    data = {
        "id": MEASURE_ID,
        "incident_id": INCIDENT_ID,
        "title": "Passwort Reset",
        "description": "Alle betroffenen Passwoerter zuruecksetzen",
        "measure_type": "corrective",
        "status": "planned",
        "responsible": "IT-Admin",
        "due_date": None,
        "completed_at": None,
        "created_at": now,
        "updated_at": now,
    }
    if overrides:
        data.update(overrides)
    return data


@pytest.fixture
def mock_db():
    from classroom_engine.database import get_db
    db = MagicMock()
    app.dependency_overrides[get_db] = lambda: db
    yield db
    app.dependency_overrides.clear()


# =============================================================================
# Helper / Utility Tests
# =============================================================================

class TestCalculateRiskLevel:
    def test_low(self):
        assert _calculate_risk_level(1, 1) == "low"
        assert _calculate_risk_level(2, 2) == "low"

    def test_medium(self):
        assert _calculate_risk_level(2, 3) == "medium"
        assert _calculate_risk_level(3, 3) == "medium"

    def test_high(self):
        assert _calculate_risk_level(3, 4) == "high"
        assert _calculate_risk_level(4, 4) == "high"

    def test_critical(self):
        assert _calculate_risk_level(4, 5) == "critical"
        assert _calculate_risk_level(5, 5) == "critical"


class TestIsNotificationRequired:
    def test_critical_requires(self):
        assert _is_notification_required("critical") is True

    def test_high_requires(self):
        assert _is_notification_required("high") is True

    def test_medium_not_required(self):
        assert _is_notification_required("medium") is False

    def test_low_not_required(self):
        assert _is_notification_required("low") is False


class TestCalculate72hDeadline:
    def test_returns_iso_string(self):
        dt = datetime(2024, 6, 1, 12, 0, 0)
        result = _calculate_72h_deadline(dt)
        assert "2024-06-04T12:00:00" in result


class TestParseJsonb:
    def test_dict_passthrough(self):
        assert _parse_jsonb({"a": 1}) == {"a": 1}

    def test_list_passthrough(self):
        assert _parse_jsonb([1, 2]) == [1, 2]

    def test_json_string(self):
        assert _parse_jsonb('{"a": 1}') == {"a": 1}

    def test_none(self):
        assert _parse_jsonb(None) is None


class TestIncidentToResponse:
    def test_parses_jsonb_fields(self):
        row = _DictRow(make_incident_row())
        result = _incident_to_response(row)
        assert isinstance(result["timeline"], list)
        assert isinstance(result["authority_notification"], dict)

    def test_converts_datetime_to_iso(self):
        row = _DictRow(make_incident_row())
        result = _incident_to_response(row)
        assert "2024-06-01" in result["detected_at"]


# =============================================================================
# Create Incident Tests
# =============================================================================

class TestCreateIncident:
    def test_create_basic(self, mock_db):
        created_row = make_incident_row()
        mock_db.execute.return_value = _DictResult([created_row])

        resp = client.post("/incidents", json={
            "title": "Datenpanne Test",
            "description": "Test",
        }, headers={"x-tenant-id": DEFAULT_TENANT, "x-user-id": "admin"})

        assert resp.status_code == 200
        data = resp.json()
        assert "incident" in data
        assert "authority_deadline" in data
        assert "hours_until_deadline" in data

    def test_create_with_detected_at(self, mock_db):
        created_row = make_incident_row()
        mock_db.execute.side_effect = [
            None,                          # INSERT
            _DictResult([created_row]),    # SELECT back
        ]

        resp = client.post("/incidents", json={
            "title": "Panne 2",
            "detected_at": "2024-06-01T10:00:00",
        })
        assert resp.status_code == 200
        assert "authority_deadline" in resp.json()

    def test_create_missing_title(self, mock_db):
        resp = client.post("/incidents", json={})
        assert resp.status_code == 422


# =============================================================================
# List Incidents Tests
# =============================================================================

class TestListIncidents:
    def test_list_empty(self, mock_db):
        mock_db.execute.side_effect = [
            _DictResult(scalar_val=0),
            _DictResult([]),
        ]
        resp = client.get("/incidents", headers={"x-tenant-id": DEFAULT_TENANT})
        assert resp.status_code == 200
        data = resp.json()
        assert data["incidents"] == []
        assert data["total"] == 0

    def test_list_with_items(self, mock_db):
        row = make_incident_row()
        mock_db.execute.side_effect = [
            _DictResult(scalar_val=1),
            _DictResult([row]),
        ]
        resp = client.get("/incidents")
        assert resp.status_code == 200
        data = resp.json()
        assert len(data["incidents"]) == 1
        assert data["total"] == 1

    def test_list_filter_by_status(self, mock_db):
        mock_db.execute.side_effect = [
            _DictResult(scalar_val=0),
            _DictResult([]),
        ]
        resp = client.get("/incidents?status=closed")
        assert resp.status_code == 200


# =============================================================================
# Get Incident Tests
# =============================================================================

class TestGetIncident:
    def test_get_existing(self, mock_db):
        row = make_incident_row()
        mock_db.execute.side_effect = [
            _DictResult([row]),       # incident
            _DictResult([]),          # measures
        ]
        resp = client.get(f"/incidents/{INCIDENT_ID}")
        assert resp.status_code == 200
        data = resp.json()
        assert "incident" in data
        assert "measures" in data
        assert "deadline_info" in data

    def test_get_not_found(self, mock_db):
        mock_db.execute.return_value = _DictResult([])
        resp = client.get(f"/incidents/{UNKNOWN_ID}")
        assert resp.status_code == 404


# =============================================================================
# Update Incident Tests
# =============================================================================

class TestUpdateIncident:
    def test_update_title(self, mock_db):
        row = make_incident_row({"title": "Updated Title"})
        mock_db.execute.side_effect = [
            _DictResult([{"id": INCIDENT_ID}]),  # check exists
            None,                                  # update
            _DictResult([row]),                    # fetch back
        ]
        resp = client.put(f"/incidents/{INCIDENT_ID}", json={"title": "Updated Title"})
        assert resp.status_code == 200
        assert resp.json()["incident"]["title"] == "Updated Title"

    def test_update_not_found(self, mock_db):
        mock_db.execute.return_value = _DictResult([])
        resp = client.put(f"/incidents/{UNKNOWN_ID}", json={"title": "X"})
        assert resp.status_code == 404


# =============================================================================
# Delete Incident Tests
# =============================================================================

class TestDeleteIncident:
    def test_delete_existing(self, mock_db):
        mock_db.execute.side_effect = [
            _DictResult([{"id": INCIDENT_ID}]),  # check exists
            None,  # delete measures
            None,  # delete incident
        ]
        resp = client.delete(f"/incidents/{INCIDENT_ID}")
        assert resp.status_code == 200
        assert resp.json()["message"] == "incident deleted"

    def test_delete_not_found(self, mock_db):
        mock_db.execute.return_value = _DictResult([])
        resp = client.delete(f"/incidents/{UNKNOWN_ID}")
        assert resp.status_code == 404


# =============================================================================
# Status Update Tests (NEW endpoint)
# =============================================================================

class TestStatusUpdate:
    def test_update_status(self, mock_db):
        row = make_incident_row({"status": "assessment"})
        mock_db.execute.side_effect = [
            _DictResult([{"id": INCIDENT_ID}]),  # check exists
            None,  # update status
            None,  # append timeline
            _DictResult([row]),  # fetch back
        ]
        resp = client.put(f"/incidents/{INCIDENT_ID}/status", json={"status": "assessment"})
        assert resp.status_code == 200
        assert resp.json()["incident"]["status"] == "assessment"

    def test_status_not_found(self, mock_db):
        mock_db.execute.return_value = _DictResult([])
        resp = client.put(f"/incidents/{UNKNOWN_ID}/status", json={"status": "closed"})
        assert resp.status_code == 404


# =============================================================================
# Risk Assessment Tests
# =============================================================================

class TestAssessRisk:
    def test_low_risk(self, mock_db):
        row = make_incident_row()
        mock_db.execute.side_effect = [
            _DictResult([row]),  # check exists
            None,  # update risk_assessment + status
            None,  # append timeline
        ]
        resp = client.post(f"/incidents/{INCIDENT_ID}/assess-risk", json={
            "likelihood": 1,
            "impact": 2,
            "notes": "Low risk",
        })
        assert resp.status_code == 200
        data = resp.json()
        assert data["risk_assessment"]["risk_level"] == "low"
        assert data["notification_required"] is False

    def test_high_risk_requires_notification(self, mock_db):
        row = make_incident_row()
        mock_db.execute.side_effect = [
            _DictResult([row]),  # check exists
            None,  # update authority_notification
            None,  # update risk_assessment + status
            None,  # append timeline
        ]
        resp = client.post(f"/incidents/{INCIDENT_ID}/assess-risk", json={
            "likelihood": 4,
            "impact": 4,
        })
        assert resp.status_code == 200
        data = resp.json()
        assert data["risk_assessment"]["risk_level"] == "high"
        assert data["notification_required"] is True
        assert data["incident_status"] == "notification_required"

    def test_critical_risk(self, mock_db):
        row = make_incident_row()
        mock_db.execute.side_effect = [
            _DictResult([row]),
            None, None, None,
        ]
        resp = client.post(f"/incidents/{INCIDENT_ID}/assess-risk", json={
            "likelihood": 5,
            "impact": 5,
        })
        assert resp.status_code == 200
        assert resp.json()["risk_assessment"]["risk_level"] == "critical"

    def test_assess_not_found(self, mock_db):
        mock_db.execute.return_value = _DictResult([])
        resp = client.post(f"/incidents/{UNKNOWN_ID}/assess-risk", json={
            "likelihood": 3, "impact": 3,
        })
        assert resp.status_code == 404


# =============================================================================
# Authority Notification Tests (Art. 33)
# =============================================================================

class TestNotifyAuthority:
    def test_notify_authority(self, mock_db):
        row = make_incident_row()
        mock_db.execute.side_effect = [
            _DictResult([row]),  # check exists
            None,  # update authority_notification + status
            None,  # append timeline
        ]
        resp = client.post(f"/incidents/{INCIDENT_ID}/notify-authority", json={
            "authority_name": "LfD Bayern",
            "reference_number": "REF-2024-001",
        })
        assert resp.status_code == 200
        data = resp.json()
        assert data["authority_notification"]["authority_name"] == "LfD Bayern"
        assert "submitted_within_72h" in data

    def test_notify_authority_not_found(self, mock_db):
        mock_db.execute.return_value = _DictResult([])
        resp = client.post(f"/incidents/{UNKNOWN_ID}/notify-authority", json={
            "authority_name": "Test",
        })
        assert resp.status_code == 404


# =============================================================================
# Data Subject Notification Tests (Art. 34)
# =============================================================================

class TestNotifySubjects:
    def test_notify_subjects(self, mock_db):
        row = make_incident_row()
        mock_db.execute.side_effect = [
            _DictResult([row]),
            None, None,
        ]
        resp = client.post(f"/incidents/{INCIDENT_ID}/notify-subjects", json={
            "notification_text": "Ihre Daten waren betroffen",
            "channel": "email",
        })
        assert resp.status_code == 200
        data = resp.json()
        assert data["data_subject_notification"]["status"] == "sent"
        assert data["data_subject_notification"]["channel"] == "email"


# =============================================================================
# Measures Tests
# =============================================================================

class TestMeasures:
    def test_add_measure(self, mock_db):
        measure_row = make_measure_row()
        mock_db.execute.side_effect = [
            _DictResult([{"id": INCIDENT_ID}]),  # check exists
            None,  # insert measure
            None,  # append timeline
            _DictResult([measure_row]),           # fetch measure
        ]
        resp = client.post(f"/incidents/{INCIDENT_ID}/measures", json={
            "title": "Passwort Reset",
            "measure_type": "corrective",
        })
        assert resp.status_code == 200
        assert resp.json()["measure"]["title"] == "Passwort Reset"

    def test_add_measure_not_found(self, mock_db):
        mock_db.execute.return_value = _DictResult([])
        resp = client.post(f"/incidents/{UNKNOWN_ID}/measures", json={
            "title": "Test",
        })
        assert resp.status_code == 404

    def test_update_measure(self, mock_db):
        updated_row = make_measure_row({"title": "Neuer Titel"})
        mock_db.execute.side_effect = [
            _DictResult([{"id": MEASURE_ID}]),  # check exists
            None,                                # update
            _DictResult([updated_row]),          # fetch back
        ]
        resp = client.put(
            f"/incidents/{INCIDENT_ID}/measures/{MEASURE_ID}",
            json={"title": "Neuer Titel"},
        )
        assert resp.status_code == 200
        assert resp.json()["measure"]["title"] == "Neuer Titel"

    def test_complete_measure(self, mock_db):
        mock_db.execute.side_effect = [
            _DictResult([{"id": MEASURE_ID}]),
            None,
        ]
        resp = client.post(f"/incidents/{INCIDENT_ID}/measures/{MEASURE_ID}/complete")
        assert resp.status_code == 200
        assert resp.json()["message"] == "measure completed"

    def test_measures_in_get_response(self, mock_db):
        inc_row = make_incident_row()
        meas_row = make_measure_row()
        mock_db.execute.side_effect = [
            _DictResult([inc_row]),
            _DictResult([meas_row]),
        ]
        resp = client.get(f"/incidents/{INCIDENT_ID}")
        assert resp.status_code == 200
        assert len(resp.json()["measures"]) == 1


# =============================================================================
# Timeline Tests
# =============================================================================

class TestTimeline:
    def test_add_timeline_entry(self, mock_db):
        mock_db.execute.side_effect = [
            _DictResult([{"id": INCIDENT_ID}]),
            None,
        ]
        resp = client.post(f"/incidents/{INCIDENT_ID}/timeline", json={
            "action": "investigation_started",
            "details": "Forensic analysis begun",
        })
        assert resp.status_code == 200
        data = resp.json()
        assert data["timeline_entry"]["action"] == "investigation_started"

    def test_timeline_not_found(self, mock_db):
        mock_db.execute.return_value = _DictResult([])
        resp = client.post(f"/incidents/{UNKNOWN_ID}/timeline", json={
            "action": "test",
        })
        assert resp.status_code == 404

    def test_auto_timeline_on_create(self, mock_db):
        """Create endpoint auto-adds incident_created timeline entry."""
        row = make_incident_row()
        mock_db.execute.return_value = _DictResult([row])

        resp = client.post("/incidents", json={"title": "Test"})
        assert resp.status_code == 200
        # The insert SQL includes a timeline with incident_created entry
        insert_call = mock_db.execute.call_args_list[0]
        sql_text = str(insert_call[0][0])
        assert "incident_incidents" in sql_text


# =============================================================================
# Close Incident Tests
# =============================================================================

class TestCloseIncident:
    def test_close_with_root_cause(self, mock_db):
        mock_db.execute.side_effect = [
            _DictResult([{"id": INCIDENT_ID}]),
            None,  # update
            None,  # timeline
        ]
        resp = client.post(f"/incidents/{INCIDENT_ID}/close", json={
            "root_cause": "Schwache Passwoerter",
            "lessons_learned": "2FA einfuehren",
        })
        assert resp.status_code == 200
        data = resp.json()
        assert data["message"] == "incident closed"
        assert data["root_cause"] == "Schwache Passwoerter"

    def test_close_minimal(self, mock_db):
        mock_db.execute.side_effect = [
            _DictResult([{"id": INCIDENT_ID}]),
            None, None,
        ]
        resp = client.post(f"/incidents/{INCIDENT_ID}/close", json={
            "root_cause": "Unbekannt",
        })
        assert resp.status_code == 200

    def test_close_not_found(self, mock_db):
        mock_db.execute.return_value = _DictResult([])
        resp = client.post(f"/incidents/{UNKNOWN_ID}/close", json={
            "root_cause": "Test",
        })
        assert resp.status_code == 404


# =============================================================================
# Statistics Tests
# =============================================================================

class TestStatistics:
    def test_empty_stats(self, mock_db):
        stats_row = {
            "total": 0, "open": 0, "closed": 0,
            "critical": 0, "high": 0, "medium": 0, "low": 0,
        }
        mock_db.execute.return_value = _DictResult([stats_row])
        resp = client.get("/incidents/stats")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 0
        assert data["by_severity"]["critical"] == 0

    def test_stats_with_data(self, mock_db):
        stats_row = {
            "total": 5, "open": 3, "closed": 2,
            "critical": 1, "high": 2, "medium": 1, "low": 1,
        }
        mock_db.execute.return_value = _DictResult([stats_row])
        resp = client.get("/incidents/stats")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 5
        assert data["open"] == 3
        assert data["closed"] == 2
        assert data["by_severity"]["critical"] == 1

    def test_stats_structure(self, mock_db):
        stats_row = {
            "total": 1, "open": 1, "closed": 0,
            "critical": 0, "high": 0, "medium": 1, "low": 0,
        }
        mock_db.execute.return_value = _DictResult([stats_row])
        resp = client.get("/incidents/stats")
        data = resp.json()
        assert set(data.keys()) == {"total", "open", "closed", "by_severity"}
        assert set(data["by_severity"].keys()) == {"critical", "high", "medium", "low"}
