"""Tests for Notfallplan routes and schemas (notfallplan_routes.py).

Covers existing schema tests plus the new incidents and templates HTTP endpoints.
"""

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

from compliance.api.notfallplan_routes import (
    ContactCreate,
    ContactUpdate,
    ScenarioCreate,
    ScenarioUpdate,
    ChecklistCreate,
    ExerciseCreate,
    IncidentCreate,
    TemplateCreate,
    router,
)

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

DEFAULT_TENANT = "default"
INCIDENT_ID = "dddddddd-0001-0001-0001-000000000001"
TEMPLATE_ID = "eeeeeeee-0001-0001-0001-000000000001"
UNKNOWN_ID = "aaaaaaaa-9999-9999-9999-999999999999"


# =============================================================================
# Helpers
# =============================================================================

def make_incident_row(overrides=None):
    data = {
        "id": INCIDENT_ID,
        "tenant_id": DEFAULT_TENANT,
        "title": "Test Incident",
        "description": "An incident occurred",
        "detected_at": datetime(2024, 1, 1),
        "detected_by": "System",
        "status": "detected",
        "severity": "medium",
        "affected_data_categories": [],
        "estimated_affected_persons": 0,
        "measures": [],
        "art34_required": False,
        "art34_justification": None,
        "reported_to_authority_at": None,
        "notified_affected_at": None,
        "closed_at": None,
        "closed_by": None,
        "lessons_learned": None,
        "created_at": datetime(2024, 1, 1),
        "updated_at": datetime(2024, 1, 1),
    }
    if overrides:
        data.update(overrides)
    row = MagicMock()
    for k, v in data.items():
        setattr(row, k, v)
    row._mapping = data
    return row


def make_template_row(overrides=None):
    data = {
        "id": TEMPLATE_ID,
        "tenant_id": DEFAULT_TENANT,
        "type": "art33",
        "title": "Art. 33 Template",
        "content": "Sehr geehrte Behoerde...",
        "created_at": datetime(2024, 1, 1),
        "updated_at": datetime(2024, 1, 1),
    }
    if overrides:
        data.update(overrides)
    row = MagicMock()
    for k, v in data.items():
        setattr(row, k, v)
    row._mapping = data
    return row


@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()


# =============================================================================
# Existing Schema Tests — ContactCreate
# =============================================================================

class TestContactCreate:
    def test_minimal_valid(self):
        req = ContactCreate(name="Max Mustermann")
        assert req.name == "Max Mustermann"
        assert req.is_primary is False
        assert req.available_24h is False
        assert req.email is None
        assert req.phone is None

    def test_full_contact(self):
        req = ContactCreate(
            name="Anna Schmidt",
            role="DSB",
            email="anna@example.com",
            phone="+49 160 12345678",
            is_primary=True,
            available_24h=True,
        )
        assert req.role == "DSB"
        assert req.is_primary is True
        assert req.available_24h is True

    def test_serialization(self):
        req = ContactCreate(name="Test Kontakt", role="IT-Leiter")
        data = req.model_dump(exclude_none=True)
        assert data["name"] == "Test Kontakt"
        assert data["role"] == "IT-Leiter"
        assert "email" not in data


# =============================================================================
# Existing Schema Tests — ContactUpdate
# =============================================================================

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

    def test_partial_update(self):
        req = ContactUpdate(phone="+49 170 9876543", available_24h=True)
        data = req.model_dump(exclude_none=True)
        assert data == {"phone": "+49 170 9876543", "available_24h": True}


# =============================================================================
# Existing Schema Tests — ScenarioCreate
# =============================================================================

class TestScenarioCreate:
    def test_minimal_valid(self):
        req = ScenarioCreate(title="Datenpanne")
        assert req.title == "Datenpanne"
        assert req.severity == "medium"
        assert req.is_active is True
        assert req.response_steps == []

    def test_with_response_steps(self):
        steps = ["Schritt 1: Incident identifizieren", "Schritt 2: DSB informieren"]
        req = ScenarioCreate(
            title="Ransomware-Angriff",
            category="system_failure",
            severity="critical",
            response_steps=steps,
            estimated_recovery_time=48,
        )
        assert req.category == "system_failure"
        assert req.severity == "critical"
        assert len(req.response_steps) == 2
        assert req.estimated_recovery_time == 48

    def test_full_serialization(self):
        req = ScenarioCreate(
            title="Phishing",
            category="data_breach",
            severity="high",
            description="Mitarbeiter wurde Opfer eines Phishing-Angriffs",
        )
        data = req.model_dump(exclude_none=True)
        assert data["severity"] == "high"
        assert data["category"] == "data_breach"


# =============================================================================
# Existing Schema Tests — ScenarioUpdate
# =============================================================================

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

    def test_severity_update(self):
        req = ScenarioUpdate(severity="low")
        data = req.model_dump(exclude_none=True)
        assert data == {"severity": "low"}

    def test_deactivate(self):
        req = ScenarioUpdate(is_active=False)
        data = req.model_dump(exclude_none=True)
        assert data["is_active"] is False


# =============================================================================
# Existing Schema Tests — ChecklistCreate
# =============================================================================

class TestChecklistCreate:
    def test_minimal_valid(self):
        req = ChecklistCreate(title="DSB benachrichtigen")
        assert req.title == "DSB benachrichtigen"
        assert req.is_required is True
        assert req.order_index == 0
        assert req.scenario_id is None

    def test_with_scenario_link(self):
        req = ChecklistCreate(
            title="IT-Team alarmieren",
            scenario_id="550e8400-e29b-41d4-a716-446655440000",
            order_index=1,
            is_required=True,
        )
        assert req.scenario_id == "550e8400-e29b-41d4-a716-446655440000"
        assert req.order_index == 1


# =============================================================================
# Existing Schema Tests — ExerciseCreate
# =============================================================================

class TestExerciseCreate:
    def test_minimal_valid(self):
        req = ExerciseCreate(title="Jahresübung 2026")
        assert req.title == "Jahresübung 2026"
        assert req.participants == []
        assert req.outcome is None

    def test_full_exercise(self):
        req = ExerciseCreate(
            title="Ransomware-Simulation",
            scenario_id="550e8400-e29b-41d4-a716-446655440000",
            participants=["Max Mustermann", "Anna Schmidt"],
            outcome="passed",
            notes="Übung verlief planmäßig",
        )
        assert req.outcome == "passed"
        assert len(req.participants) == 2
        assert req.notes == "Übung verlief planmäßig"


# =============================================================================
# New Schema Tests — IncidentCreate / TemplateCreate
# =============================================================================

class TestIncidentCreateSchema:
    def test_incident_create_minimal(self):
        inc = IncidentCreate(title="Breach")
        assert inc.title == "Breach"
        assert inc.status == "detected"
        assert inc.severity == "medium"
        assert inc.estimated_affected_persons == 0
        assert inc.art34_required is False
        assert inc.affected_data_categories == []
        assert inc.measures == []

    def test_incident_create_full(self):
        inc = IncidentCreate(
            title="Big Breach",
            description="Ransomware attack",
            detected_by="SIEM",
            status="assessed",
            severity="critical",
            affected_data_categories=["personal", "health"],
            estimated_affected_persons=1000,
            measures=["isolation", "backup restore"],
            art34_required=True,
            art34_justification="High risk to data subjects",
        )
        assert inc.severity == "critical"
        assert inc.estimated_affected_persons == 1000
        assert len(inc.affected_data_categories) == 2
        assert inc.art34_required is True

    def test_incident_create_serialization_excludes_none(self):
        inc = IncidentCreate(title="T")
        data = inc.model_dump(exclude_none=True)
        assert data["title"] == "T"
        assert "art34_justification" not in data
        assert "description" not in data


class TestTemplateCreateSchema:
    def test_template_create_requires_title_content(self):
        t = TemplateCreate(title="T", content="C", type="art33")
        assert t.title == "T"
        assert t.content == "C"
        assert t.type == "art33"

    def test_template_create_default_type(self):
        t = TemplateCreate(title="T", content="C")
        assert t.type == "art33"

    def test_template_create_art34_type(self):
        t = TemplateCreate(title="Notification Letter", content="Dear...", type="art34")
        assert t.type == "art34"


# =============================================================================
# Incidents — GET /notfallplan/incidents
# =============================================================================

class TestListIncidents:
    def test_list_incidents_returns_empty_list(self, mock_db):
        mock_db.execute.return_value.fetchall.return_value = []
        resp = client.get("/notfallplan/incidents")
        assert resp.status_code == 200
        assert resp.json() == []

    def test_list_incidents_returns_one_incident(self, mock_db):
        row = make_incident_row()
        mock_db.execute.return_value.fetchall.return_value = [row]
        resp = client.get("/notfallplan/incidents")
        assert resp.status_code == 200
        data = resp.json()
        assert len(data) == 1
        assert data[0]["title"] == "Test Incident"
        assert data[0]["status"] == "detected"
        assert data[0]["severity"] == "medium"

    def test_list_incidents_returns_multiple(self, mock_db):
        rows = [
            make_incident_row({"id": "id-1", "title": "Incident A"}),
            make_incident_row({"id": "id-2", "title": "Incident B"}),
        ]
        mock_db.execute.return_value.fetchall.return_value = rows
        resp = client.get("/notfallplan/incidents")
        assert resp.status_code == 200
        assert len(resp.json()) == 2

    def test_list_incidents_filter_by_status(self, mock_db):
        mock_db.execute.return_value.fetchall.return_value = []
        resp = client.get("/notfallplan/incidents?status=closed")
        assert resp.status_code == 200
        call_params = mock_db.execute.call_args[0][1]
        assert call_params.get("status") == "closed"

    def test_list_incidents_filter_by_severity(self, mock_db):
        mock_db.execute.return_value.fetchall.return_value = []
        resp = client.get("/notfallplan/incidents?severity=high")
        assert resp.status_code == 200
        call_params = mock_db.execute.call_args[0][1]
        assert call_params.get("severity") == "high"

    def test_list_incidents_filter_combined(self, mock_db):
        mock_db.execute.return_value.fetchall.return_value = []
        resp = client.get("/notfallplan/incidents?status=detected&severity=critical")
        assert resp.status_code == 200
        call_params = mock_db.execute.call_args[0][1]
        assert call_params.get("status") == "detected"
        assert call_params.get("severity") == "critical"

    def test_list_incidents_uses_default_tenant(self, mock_db):
        mock_db.execute.return_value.fetchall.return_value = []
        resp = client.get("/notfallplan/incidents")
        assert resp.status_code == 200
        call_params = mock_db.execute.call_args[0][1]
        assert call_params.get("tenant_id") == DEFAULT_TENANT

    def test_list_incidents_custom_tenant_header(self, mock_db):
        mock_db.execute.return_value.fetchall.return_value = []
        resp = client.get("/notfallplan/incidents", headers={"X-Tenant-ID": "my-tenant"})
        assert resp.status_code == 200
        call_params = mock_db.execute.call_args[0][1]
        assert call_params.get("tenant_id") == "my-tenant"

    def test_list_incidents_all_fields_present(self, mock_db):
        row = make_incident_row()
        mock_db.execute.return_value.fetchall.return_value = [row]
        resp = client.get("/notfallplan/incidents")
        item = resp.json()[0]
        expected_fields = (
            "id", "tenant_id", "title", "description", "detected_at",
            "detected_by", "status", "severity", "affected_data_categories",
            "estimated_affected_persons", "measures", "art34_required",
            "art34_justification", "reported_to_authority_at",
            "notified_affected_at", "closed_at", "closed_by",
            "lessons_learned", "created_at", "updated_at",
        )
        for field in expected_fields:
            assert field in item, f"Missing field: {field}"


# =============================================================================
# Incidents — POST /notfallplan/incidents
# =============================================================================

class TestCreateIncident:
    def test_create_incident_minimal(self, mock_db):
        row = make_incident_row()
        mock_db.execute.return_value.fetchone.return_value = row
        resp = client.post("/notfallplan/incidents", json={"title": "New Incident"})
        assert resp.status_code == 201
        assert resp.json()["title"] == "Test Incident"

    def test_create_incident_full_payload(self, mock_db):
        row = make_incident_row({
            "title": "Critical Breach",
            "description": "Database exposed",
            "detected_by": "SOC Team",
            "status": "assessed",
            "severity": "critical",
            "estimated_affected_persons": 500,
        })
        mock_db.execute.return_value.fetchone.return_value = row
        resp = client.post("/notfallplan/incidents", json={
            "title": "Critical Breach",
            "description": "Database exposed",
            "detected_by": "SOC Team",
            "status": "assessed",
            "severity": "critical",
            "estimated_affected_persons": 500,
        })
        assert resp.status_code == 201
        data = resp.json()
        assert data["severity"] == "critical"
        assert data["estimated_affected_persons"] == 500

    def test_create_incident_missing_title_returns_422(self, mock_db):
        resp = client.post("/notfallplan/incidents", json={"description": "No title here"})
        assert resp.status_code == 422

    def test_create_incident_default_status_passed_to_db(self, mock_db):
        row = make_incident_row()
        mock_db.execute.return_value.fetchone.return_value = row
        client.post("/notfallplan/incidents", json={"title": "T"})
        call_params = mock_db.execute.call_args[0][1]
        assert call_params["status"] == "detected"
        assert call_params["severity"] == "medium"

    def test_create_incident_commits(self, mock_db):
        row = make_incident_row()
        mock_db.execute.return_value.fetchone.return_value = row
        client.post("/notfallplan/incidents", json={"title": "T"})
        mock_db.commit.assert_called_once()

    def test_create_incident_with_art34_required(self, mock_db):
        row = make_incident_row({"art34_required": True, "art34_justification": "Hohe Risikobewertung"})
        mock_db.execute.return_value.fetchone.return_value = row
        resp = client.post("/notfallplan/incidents", json={
            "title": "High Risk",
            "art34_required": True,
            "art34_justification": "Hohe Risikobewertung",
        })
        assert resp.status_code == 201
        assert resp.json()["art34_required"] is True

    def test_create_incident_passes_tenant_id(self, mock_db):
        row = make_incident_row()
        mock_db.execute.return_value.fetchone.return_value = row
        client.post("/notfallplan/incidents",
                    json={"title": "T"},
                    headers={"X-Tenant-ID": "custom-org"})
        call_params = mock_db.execute.call_args[0][1]
        assert call_params["tenant_id"] == "custom-org"


# =============================================================================
# Incidents — PUT /notfallplan/incidents/{id}
# =============================================================================

class TestUpdateIncident:
    def test_update_incident_success(self, mock_db):
        existing = MagicMock()
        updated_row = make_incident_row({"status": "assessed"})
        mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
        resp = client.put(f"/notfallplan/incidents/{INCIDENT_ID}", json={"status": "assessed"})
        assert resp.status_code == 200
        assert resp.json()["status"] == "assessed"

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

    def test_update_incident_empty_body_returns_400(self, mock_db):
        existing = MagicMock()
        mock_db.execute.return_value.fetchone.return_value = existing
        resp = client.put(f"/notfallplan/incidents/{INCIDENT_ID}", json={})
        assert resp.status_code == 400

    def test_update_incident_status_to_reported_auto_sets_timestamp(self, mock_db):
        existing = MagicMock()
        updated_row = make_incident_row({"status": "reported"})
        mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
        resp = client.put(f"/notfallplan/incidents/{INCIDENT_ID}", json={"status": "reported"})
        assert resp.status_code == 200
        call_params = mock_db.execute.call_args[0][1]
        assert "reported_to_authority_at" in call_params

    def test_update_incident_status_to_closed_auto_sets_closed_at(self, mock_db):
        existing = MagicMock()
        updated_row = make_incident_row({"status": "closed"})
        mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
        resp = client.put(f"/notfallplan/incidents/{INCIDENT_ID}", json={"status": "closed"})
        assert resp.status_code == 200
        call_params = mock_db.execute.call_args[0][1]
        assert "closed_at" in call_params

    def test_update_incident_lessons_learned(self, mock_db):
        existing = MagicMock()
        updated_row = make_incident_row({"lessons_learned": "Besseres Monitoring nötig"})
        mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
        resp = client.put(
            f"/notfallplan/incidents/{INCIDENT_ID}",
            json={"lessons_learned": "Besseres Monitoring nötig"},
        )
        assert resp.status_code == 200
        assert resp.json()["lessons_learned"] == "Besseres Monitoring nötig"

    def test_update_incident_severity(self, mock_db):
        existing = MagicMock()
        updated_row = make_incident_row({"severity": "high"})
        mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
        resp = client.put(f"/notfallplan/incidents/{INCIDENT_ID}", json={"severity": "high"})
        assert resp.status_code == 200
        assert resp.json()["severity"] == "high"

    def test_update_incident_commits(self, mock_db):
        existing = MagicMock()
        updated_row = make_incident_row()
        mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
        client.put(f"/notfallplan/incidents/{INCIDENT_ID}", json={"severity": "low"})
        mock_db.commit.assert_called()

    def test_update_incident_always_sets_updated_at(self, mock_db):
        existing = MagicMock()
        updated_row = make_incident_row()
        mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
        client.put(f"/notfallplan/incidents/{INCIDENT_ID}", json={"title": "Renamed"})
        call_params = mock_db.execute.call_args[0][1]
        assert "updated_at" in call_params


# =============================================================================
# Incidents — DELETE /notfallplan/incidents/{id}
# =============================================================================

class TestDeleteIncident:
    def test_delete_incident_success_returns_204(self, mock_db):
        mock_db.execute.return_value.rowcount = 1
        resp = client.delete(f"/notfallplan/incidents/{INCIDENT_ID}")
        assert resp.status_code == 204

    def test_delete_incident_not_found_returns_404(self, mock_db):
        mock_db.execute.return_value.rowcount = 0
        resp = client.delete(f"/notfallplan/incidents/{UNKNOWN_ID}")
        assert resp.status_code == 404

    def test_delete_incident_commits(self, mock_db):
        mock_db.execute.return_value.rowcount = 1
        client.delete(f"/notfallplan/incidents/{INCIDENT_ID}")
        mock_db.commit.assert_called_once()

    def test_delete_incident_commits_even_when_not_found(self, mock_db):
        # Commit is called before the rowcount check in the implementation
        mock_db.execute.return_value.rowcount = 0
        client.delete(f"/notfallplan/incidents/{UNKNOWN_ID}")
        mock_db.commit.assert_called_once()

    def test_delete_incident_passes_tenant_id(self, mock_db):
        mock_db.execute.return_value.rowcount = 1
        client.delete(
            f"/notfallplan/incidents/{INCIDENT_ID}",
            headers={"X-Tenant-ID": "acme"},
        )
        call_params = mock_db.execute.call_args[0][1]
        assert call_params["tenant_id"] == "acme"


# =============================================================================
# Templates — GET /notfallplan/templates
# =============================================================================

class TestListTemplates:
    def test_list_templates_returns_empty_list(self, mock_db):
        mock_db.execute.return_value.fetchall.return_value = []
        resp = client.get("/notfallplan/templates")
        assert resp.status_code == 200
        assert resp.json() == []

    def test_list_templates_returns_one(self, mock_db):
        row = make_template_row()
        mock_db.execute.return_value.fetchall.return_value = [row]
        resp = client.get("/notfallplan/templates")
        assert resp.status_code == 200
        data = resp.json()
        assert len(data) == 1
        assert data[0]["title"] == "Art. 33 Template"
        assert data[0]["type"] == "art33"

    def test_list_templates_returns_multiple(self, mock_db):
        rows = [
            make_template_row({"id": "id-1", "type": "art33", "title": "Meldung Art.33"}),
            make_template_row({"id": "id-2", "type": "art34", "title": "Meldung Art.34"}),
        ]
        mock_db.execute.return_value.fetchall.return_value = rows
        resp = client.get("/notfallplan/templates")
        assert resp.status_code == 200
        assert len(resp.json()) == 2

    def test_list_templates_filter_by_type(self, mock_db):
        mock_db.execute.return_value.fetchall.return_value = []
        resp = client.get("/notfallplan/templates?type=art34")
        assert resp.status_code == 200
        call_params = mock_db.execute.call_args[0][1]
        assert call_params.get("type") == "art34"

    def test_list_templates_without_type_no_type_param_sent(self, mock_db):
        mock_db.execute.return_value.fetchall.return_value = []
        resp = client.get("/notfallplan/templates")
        assert resp.status_code == 200
        call_params = mock_db.execute.call_args[0][1]
        assert "type" not in call_params

    def test_list_templates_uses_default_tenant(self, mock_db):
        mock_db.execute.return_value.fetchall.return_value = []
        client.get("/notfallplan/templates")
        call_params = mock_db.execute.call_args[0][1]
        assert call_params["tenant_id"] == DEFAULT_TENANT

    def test_list_templates_custom_tenant_header(self, mock_db):
        mock_db.execute.return_value.fetchall.return_value = []
        client.get("/notfallplan/templates", headers={"X-Tenant-ID": "acme"})
        call_params = mock_db.execute.call_args[0][1]
        assert call_params["tenant_id"] == "acme"

    def test_list_templates_all_fields_present(self, mock_db):
        row = make_template_row()
        mock_db.execute.return_value.fetchall.return_value = [row]
        resp = client.get("/notfallplan/templates")
        item = resp.json()[0]
        for field in ("id", "tenant_id", "type", "title", "content", "created_at", "updated_at"):
            assert field in item, f"Missing field: {field}"


# =============================================================================
# Templates — POST /notfallplan/templates
# =============================================================================

class TestCreateTemplate:
    def test_create_template_success(self, mock_db):
        row = make_template_row()
        mock_db.execute.return_value.fetchone.return_value = row
        resp = client.post("/notfallplan/templates", json={
            "title": "Art. 33 Template",
            "content": "Sehr geehrte Behoerde...",
            "type": "art33",
        })
        assert resp.status_code == 201
        assert resp.json()["title"] == "Art. 33 Template"

    def test_create_template_missing_title_returns_422(self, mock_db):
        resp = client.post("/notfallplan/templates", json={
            "content": "Some content",
            "type": "art33",
        })
        assert resp.status_code == 422

    def test_create_template_missing_content_returns_422(self, mock_db):
        resp = client.post("/notfallplan/templates", json={
            "title": "Template",
            "type": "art33",
        })
        assert resp.status_code == 422

    def test_create_template_missing_type_uses_default_art33(self, mock_db):
        row = make_template_row()
        mock_db.execute.return_value.fetchone.return_value = row
        resp = client.post("/notfallplan/templates", json={"title": "T", "content": "C"})
        assert resp.status_code == 201
        call_params = mock_db.execute.call_args[0][1]
        assert call_params["type"] == "art33"

    def test_create_template_art34_type(self, mock_db):
        row = make_template_row({"type": "art34"})
        mock_db.execute.return_value.fetchone.return_value = row
        resp = client.post("/notfallplan/templates", json={
            "title": "Art. 34 Notification",
            "content": "Sehr geehrte Betroffene...",
            "type": "art34",
        })
        assert resp.status_code == 201
        assert resp.json()["type"] == "art34"

    def test_create_template_commits(self, mock_db):
        row = make_template_row()
        mock_db.execute.return_value.fetchone.return_value = row
        client.post("/notfallplan/templates", json={"title": "T", "content": "C", "type": "art33"})
        mock_db.commit.assert_called_once()

    def test_create_template_passes_tenant_id(self, mock_db):
        row = make_template_row()
        mock_db.execute.return_value.fetchone.return_value = row
        client.post(
            "/notfallplan/templates",
            json={"title": "T", "content": "C", "type": "art33"},
            headers={"X-Tenant-ID": "my-org"},
        )
        call_params = mock_db.execute.call_args[0][1]
        assert call_params["tenant_id"] == "my-org"


# =============================================================================
# Templates — PUT /notfallplan/templates/{id}
# =============================================================================

class TestUpdateTemplate:
    def test_update_template_title_success(self, mock_db):
        existing = MagicMock()
        updated_row = make_template_row({"title": "Updated Title"})
        mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
        resp = client.put(f"/notfallplan/templates/{TEMPLATE_ID}", json={"title": "Updated Title"})
        assert resp.status_code == 200
        assert resp.json()["title"] == "Updated Title"

    def test_update_template_not_found_returns_404(self, mock_db):
        mock_db.execute.return_value.fetchone.return_value = None
        resp = client.put(f"/notfallplan/templates/{UNKNOWN_ID}", json={"title": "X"})
        assert resp.status_code == 404

    def test_update_template_empty_body_returns_400(self, mock_db):
        existing = MagicMock()
        mock_db.execute.return_value.fetchone.return_value = existing
        resp = client.put(f"/notfallplan/templates/{TEMPLATE_ID}", json={})
        assert resp.status_code == 400

    def test_update_template_content(self, mock_db):
        existing = MagicMock()
        updated_row = make_template_row({"content": "New body text"})
        mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
        resp = client.put(
            f"/notfallplan/templates/{TEMPLATE_ID}",
            json={"content": "New body text"},
        )
        assert resp.status_code == 200
        assert resp.json()["content"] == "New body text"

    def test_update_template_type(self, mock_db):
        existing = MagicMock()
        updated_row = make_template_row({"type": "internal"})
        mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
        resp = client.put(f"/notfallplan/templates/{TEMPLATE_ID}", json={"type": "internal"})
        assert resp.status_code == 200

    def test_update_template_commits(self, mock_db):
        existing = MagicMock()
        updated_row = make_template_row()
        mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
        client.put(f"/notfallplan/templates/{TEMPLATE_ID}", json={"title": "New"})
        mock_db.commit.assert_called()

    def test_update_template_sets_updated_at(self, mock_db):
        existing = MagicMock()
        updated_row = make_template_row()
        mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
        client.put(f"/notfallplan/templates/{TEMPLATE_ID}", json={"title": "New"})
        call_params = mock_db.execute.call_args[0][1]
        assert "updated_at" in call_params


# =============================================================================
# Templates — DELETE /notfallplan/templates/{id}
# =============================================================================

class TestDeleteTemplate:
    def test_delete_template_success_returns_204(self, mock_db):
        mock_db.execute.return_value.rowcount = 1
        resp = client.delete(f"/notfallplan/templates/{TEMPLATE_ID}")
        assert resp.status_code == 204

    def test_delete_template_not_found_returns_404(self, mock_db):
        mock_db.execute.return_value.rowcount = 0
        resp = client.delete(f"/notfallplan/templates/{UNKNOWN_ID}")
        assert resp.status_code == 404

    def test_delete_template_commits_on_success(self, mock_db):
        mock_db.execute.return_value.rowcount = 1
        client.delete(f"/notfallplan/templates/{TEMPLATE_ID}")
        mock_db.commit.assert_called_once()

    def test_delete_template_commits_even_when_not_found(self, mock_db):
        mock_db.execute.return_value.rowcount = 0
        client.delete(f"/notfallplan/templates/{UNKNOWN_ID}")
        mock_db.commit.assert_called_once()

    def test_delete_template_passes_correct_tenant_id(self, mock_db):
        mock_db.execute.return_value.rowcount = 1
        client.delete(
            f"/notfallplan/templates/{TEMPLATE_ID}",
            headers={"X-Tenant-ID": "acme"},
        )
        call_params = mock_db.execute.call_args[0][1]
        assert call_params["tenant_id"] == "acme"
