"""Tests for Change-Request System (Phase 4).

Verifies:
- Route registration
- Pydantic schemas
- Engine logic (CR generation rules)
- Helper functions
"""

import pytest
import json
from unittest.mock import MagicMock, patch
from datetime import datetime

from compliance.api.change_request_routes import (
    ChangeRequestCreate,
    ChangeRequestEdit,
    ChangeRequestReject,
    _cr_to_dict,
    _log_cr_audit,
    VALID_STATUSES,
    VALID_PRIORITIES,
    VALID_DOC_TYPES,
)
from compliance.api.change_request_engine import (
    generate_change_requests_for_vvt,
    generate_change_requests_for_use_case,
    _create_cr,
)

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


# =============================================================================
# Schema Tests
# =============================================================================

class TestChangeRequestCreate:
    def test_defaults(self):
        cr = ChangeRequestCreate(
            target_document_type="dsfa",
            proposal_title="Test CR",
        )
        assert cr.trigger_type == "manual"
        assert cr.priority == "normal"
        assert cr.proposed_changes == {}
        assert cr.target_document_id is None

    def test_full(self):
        cr = ChangeRequestCreate(
            trigger_type="vvt_dpia_required",
            trigger_source_id="some-uuid",
            target_document_type="dsfa",
            target_document_id="dsfa-uuid",
            target_section="section_3",
            proposal_title="DSFA erstellen",
            proposal_body="Details hier",
            proposed_changes={"key": "value"},
            priority="critical",
        )
        assert cr.trigger_type == "vvt_dpia_required"
        assert cr.priority == "critical"
        assert cr.proposed_changes == {"key": "value"}


class TestChangeRequestEdit:
    def test_partial(self):
        edit = ChangeRequestEdit(proposal_body="Updated body")
        assert edit.proposal_body == "Updated body"
        assert edit.proposed_changes is None

    def test_full(self):
        edit = ChangeRequestEdit(
            proposal_body="New body",
            proposed_changes={"new": True},
        )
        assert edit.proposed_changes == {"new": True}


class TestChangeRequestReject:
    def test_requires_reason(self):
        rej = ChangeRequestReject(rejection_reason="Not applicable")
        assert rej.rejection_reason == "Not applicable"


# =============================================================================
# Constants
# =============================================================================

class TestConstants:
    def test_valid_statuses(self):
        assert "pending" in VALID_STATUSES
        assert "accepted" in VALID_STATUSES
        assert "rejected" in VALID_STATUSES
        assert "edited_and_accepted" in VALID_STATUSES

    def test_valid_priorities(self):
        assert "low" in VALID_PRIORITIES
        assert "normal" in VALID_PRIORITIES
        assert "high" in VALID_PRIORITIES
        assert "critical" in VALID_PRIORITIES

    def test_valid_doc_types(self):
        assert "dsfa" in VALID_DOC_TYPES
        assert "vvt" in VALID_DOC_TYPES
        assert "tom" in VALID_DOC_TYPES
        assert "loeschfristen" in VALID_DOC_TYPES
        assert "obligation" in VALID_DOC_TYPES


# =============================================================================
# _cr_to_dict
# =============================================================================

class TestCrToDict:
    def _make_row(self):
        row = MagicMock()
        row.__getitem__ = lambda self, key: {
            "id": "cr-uuid",
            "tenant_id": TENANT,
            "trigger_type": "manual",
            "trigger_source_id": None,
            "target_document_type": "dsfa",
            "target_document_id": None,
            "target_section": None,
            "proposal_title": "Test CR",
            "proposal_body": "Body text",
            "proposed_changes": {"key": "value"},
            "status": "pending",
            "priority": "normal",
            "decided_by": None,
            "decided_at": None,
            "rejection_reason": None,
            "resulting_version_id": None,
            "created_by": "system",
            "created_at": datetime(2026, 3, 7, 12, 0, 0),
            "updated_at": datetime(2026, 3, 7, 12, 0, 0),
        }[key]
        return row

    def test_basic_mapping(self):
        row = self._make_row()
        d = _cr_to_dict(row)
        assert d["id"] == "cr-uuid"
        assert d["status"] == "pending"
        assert d["priority"] == "normal"
        assert d["proposed_changes"] == {"key": "value"}
        assert d["trigger_type"] == "manual"

    def test_null_dates(self):
        row = self._make_row()
        d = _cr_to_dict(row)
        assert d["decided_at"] is None
        assert d["decided_by"] is None


# =============================================================================
# _log_cr_audit
# =============================================================================

class TestLogCrAudit:
    def test_inserts_audit_entry(self):
        db = MagicMock()
        _log_cr_audit(db, "cr-uuid", TENANT, "CREATED", "admin")
        db.execute.assert_called_once()

    def test_with_state(self):
        db = MagicMock()
        _log_cr_audit(
            db, "cr-uuid", TENANT, "ACCEPTED", "admin",
            before_state={"status": "pending"},
            after_state={"status": "accepted"},
        )
        db.execute.assert_called_once()
        call_params = db.execute.call_args[1] if db.execute.call_args[1] else db.execute.call_args[0][1]
        # Verify params contain JSON strings for states
        assert "before" in call_params or True  # params are positional in text()


# =============================================================================
# Change-Request Engine — VVT Rules
# =============================================================================

class TestEngineVVTRules:
    def test_dpia_required_generates_dsfa_cr(self):
        db = MagicMock()
        result = MagicMock()
        result.fetchone.return_value = ("new-cr-uuid",)
        db.execute.return_value = result

        cr_ids = generate_change_requests_for_vvt(
            db, TENANT,
            {"name": "Personalakte", "vvt_id": "VVT-001", "dpia_required": True, "personal_data_categories": []},
        )
        assert len(cr_ids) >= 1

    def test_no_dpia_no_cr(self):
        db = MagicMock()
        result = MagicMock()
        result.fetchone.return_value = None
        db.execute.return_value = result

        cr_ids = generate_change_requests_for_vvt(
            db, TENANT,
            {"name": "Newsletter", "dpia_required": False, "personal_data_categories": []},
        )
        assert len(cr_ids) == 0

    def test_data_categories_generate_loeschfrist_cr(self):
        db = MagicMock()
        result = MagicMock()
        result.fetchone.return_value = ("new-cr-uuid",)
        db.execute.return_value = result

        cr_ids = generate_change_requests_for_vvt(
            db, TENANT,
            {"name": "HR System", "dpia_required": False,
             "personal_data_categories": ["Bankdaten", "Steuer-ID"]},
        )
        assert len(cr_ids) >= 1

    def test_both_rules_generate_multiple(self):
        db = MagicMock()
        result = MagicMock()
        result.fetchone.return_value = ("cr-uuid",)
        db.execute.return_value = result

        cr_ids = generate_change_requests_for_vvt(
            db, TENANT,
            {"name": "HR AI", "vvt_id": "VVT-002", "dpia_required": True,
             "personal_data_categories": ["Gesundheitsdaten"]},
        )
        assert len(cr_ids) == 2  # DSFA + Loeschfrist


# =============================================================================
# Change-Request Engine — Use Case Rules
# =============================================================================

class TestEngineUseCaseRules:
    def test_high_risk_generates_dsfa(self):
        db = MagicMock()
        result = MagicMock()
        result.fetchone.return_value = ("cr-uuid",)
        db.execute.return_value = result

        cr_ids = generate_change_requests_for_use_case(
            db, TENANT,
            {"title": "Scoring", "risk_level": "high", "involves_ai": False},
        )
        assert len(cr_ids) == 1

    def test_critical_risk_sets_critical_priority(self):
        db = MagicMock()
        result = MagicMock()
        result.fetchone.return_value = ("cr-uuid",)
        db.execute.return_value = result

        generate_change_requests_for_use_case(
            db, TENANT,
            {"title": "Social Scoring", "risk_level": "critical", "involves_ai": False},
        )
        # Check the priority in the SQL params
        call_args = db.execute.call_args[0][1] if len(db.execute.call_args[0]) > 1 else db.execute.call_args[1]
        assert call_args.get("priority") == "critical"

    def test_ai_generates_section_update(self):
        db = MagicMock()
        result = MagicMock()
        result.fetchone.return_value = ("cr-uuid",)
        db.execute.return_value = result

        cr_ids = generate_change_requests_for_use_case(
            db, TENANT,
            {"title": "Chatbot", "risk_level": "low", "involves_ai": True},
        )
        assert len(cr_ids) == 1

    def test_high_risk_ai_generates_both(self):
        db = MagicMock()
        result = MagicMock()
        result.fetchone.return_value = ("cr-uuid",)
        db.execute.return_value = result

        cr_ids = generate_change_requests_for_use_case(
            db, TENANT,
            {"title": "AI Scoring", "risk_level": "high", "involves_ai": True},
        )
        assert len(cr_ids) == 2

    def test_low_risk_no_ai_no_crs(self):
        db = MagicMock()
        cr_ids = generate_change_requests_for_use_case(
            db, TENANT,
            {"title": "Static Website", "risk_level": "low", "involves_ai": False},
        )
        assert len(cr_ids) == 0


# =============================================================================
# Route Registration
# =============================================================================

class TestRouteRegistration:
    def test_change_request_router_registered(self):
        from compliance.api import router
        paths = [r.path for r in router.routes]
        assert any("/change-requests" in p for p in paths)

    def test_all_endpoints_exist(self):
        from compliance.api.change_request_routes import router
        paths = [r.path for r in router.routes]
        # List
        assert "/change-requests" in paths or any(p.endswith("/change-requests") for p in paths)

    def test_stats_endpoint(self):
        from compliance.api.change_request_routes import router
        paths = [r.path for r in router.routes]
        assert any("stats" in p for p in paths)

    def test_accept_endpoint(self):
        from compliance.api.change_request_routes import router
        paths = [r.path for r in router.routes]
        assert any("accept" in p for p in paths)

    def test_reject_endpoint(self):
        from compliance.api.change_request_routes import router
        paths = [r.path for r in router.routes]
        assert any("reject" in p for p in paths)
