"""Tests for Service Module Registry routes (module_routes.py)."""

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

from compliance.api.module_routes import router as module_router
from classroom_engine.database import get_db

# ---------------------------------------------------------------------------
# App setup with mocked DB dependency
# ---------------------------------------------------------------------------

app = FastAPI()
app.include_router(module_router)

mock_db = MagicMock()


def override_get_db():
    yield mock_db


app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)

MODULE_UUID = "aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb"
REG_UUID = "cccccccc-4444-5555-6666-dddddddddddd"
TENANT_ID = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"
NOW = datetime(2024, 1, 15, 10, 0, 0)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def make_module(overrides=None):
    """Return a MagicMock that behaves like a ServiceModuleDB instance."""
    m = MagicMock()
    m.id = MODULE_UUID
    m.name = "consent-service"
    m.display_name = "Go Consent Service"
    m.description = "Manages user consents"
    m.service_type = MagicMock()
    m.service_type.value = "backend"
    m.port = 8080
    m.technology_stack = ["Go", "Gin", "PostgreSQL"]
    m.repository_path = "/consent-service"
    m.docker_image = "breakpilot-consent-service"
    m.data_categories = ["consent_records", "personal_data"]
    m.processes_pii = True
    m.processes_health_data = False
    m.ai_components = False
    m.criticality = "critical"
    m.owner_team = "Backend Team"
    m.owner_contact = "backend@breakpilot.app"
    m.is_active = True
    m.compliance_score = 85.0
    m.last_compliance_check = None
    m.created_at = NOW
    m.updated_at = NOW
    m.regulation_mappings = []
    m.module_risks = []
    if overrides:
        for k, v in overrides.items():
            setattr(m, k, v)
    return m


def make_overview():
    return {
        "total_modules": 5,
        "modules_by_type": {"backend": 3, "ai": 2},
        "modules_by_criticality": {"critical": 1, "high": 2, "medium": 2},
        "modules_processing_pii": 3,
        "modules_with_ai": 2,
        "average_compliance_score": 78.5,
        "regulations_coverage": {"GDPR": 3, "AI_ACT": 2},
    }


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

class TestListModules:
    """Tests for GET /modules."""

    def test_list_empty_db(self):
        with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo:
            instance = MockRepo.return_value
            instance.get_all.return_value = []

            response = client.get("/modules")

        assert response.status_code == 200
        data = response.json()
        assert data["modules"] == []
        assert data["total"] == 0

    def test_list_with_module(self):
        with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo:
            instance = MockRepo.return_value
            instance.get_all.return_value = [make_module()]

            response = client.get("/modules")

        assert response.status_code == 200
        data = response.json()
        assert data["total"] == 1
        m = data["modules"][0]
        assert m["name"] == "consent-service"
        assert m["display_name"] == "Go Consent Service"
        assert m["is_active"] is True
        assert m["processes_pii"] is True

    def test_list_filter_processes_pii_true(self):
        """processes_pii=true filter is forwarded."""
        with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo:
            instance = MockRepo.return_value
            pii_module = make_module({"processes_pii": True})
            instance.get_all.return_value = [pii_module]

            response = client.get("/modules", params={"processes_pii": "true"})

        assert response.status_code == 200
        data = response.json()
        assert data["modules"][0]["processes_pii"] is True

    def test_list_filter_ai_components(self):
        """ai_components filter is forwarded."""
        with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo:
            instance = MockRepo.return_value
            ai_module = make_module({"ai_components": True})
            instance.get_all.return_value = [ai_module]

            response = client.get("/modules", params={"ai_components": "true"})

        assert response.status_code == 200
        data = response.json()
        assert data["modules"][0]["ai_components"] is True

    def test_list_multiple_modules(self):
        """Multiple modules returned correctly."""
        with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo:
            instance = MockRepo.return_value
            m1 = make_module({"name": "service-a", "display_name": "Service A"})
            m2 = make_module({"name": "service-b", "display_name": "Service B"})
            instance.get_all.return_value = [m1, m2]

            response = client.get("/modules")

        assert response.status_code == 200
        assert response.json()["total"] == 2


class TestModuleOverview:
    """Tests for GET /modules/overview."""

    def test_overview_returns_stats(self):
        with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo:
            instance = MockRepo.return_value
            instance.get_overview.return_value = make_overview()

            response = client.get("/modules/overview")

        assert response.status_code == 200
        data = response.json()
        assert data["total_modules"] == 5
        assert data["modules_processing_pii"] == 3

    def test_overview_empty(self):
        with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo:
            instance = MockRepo.return_value
            empty = {
                "total_modules": 0,
                "modules_by_type": {},
                "modules_by_criticality": {},
                "modules_processing_pii": 0,
                "modules_with_ai": 0,
                "average_compliance_score": None,
                "regulations_coverage": {},
            }
            instance.get_overview.return_value = empty

            response = client.get("/modules/overview")

        assert response.status_code == 200
        data = response.json()
        assert data["total_modules"] == 0
        assert data["modules_processing_pii"] == 0


class TestGetModuleDetail:
    """Tests for GET /modules/{module_id}."""

    def test_get_existing_module(self):
        module = make_module()
        with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo:
            instance = MockRepo.return_value
            instance.get_with_regulations.return_value = module

            response = client.get(f"/modules/{MODULE_UUID}")

        assert response.status_code == 200
        data = response.json()
        assert data["id"] == MODULE_UUID
        assert data["name"] == "consent-service"
        assert "regulations" in data
        assert "risks" in data

    def test_get_module_not_found(self):
        with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo:
            instance = MockRepo.return_value
            instance.get_with_regulations.return_value = None
            instance.get_by_name.return_value = None

            response = client.get("/modules/nonexistent-id")

        assert response.status_code == 404

    def test_get_module_fallback_to_name_lookup(self):
        """Falls back to name lookup when ID lookup returns None."""
        module = make_module()
        with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo:
            instance = MockRepo.return_value
            # First get_with_regulations(id) → None, then get_by_name → module, then get_with_regulations(id) → module
            instance.get_with_regulations.side_effect = [None, module]
            instance.get_by_name.return_value = module

            response = client.get("/modules/consent-service")

        assert response.status_code == 200


class TestActivateDeactivate:
    """Tests for POST /modules/{id}/activate and /deactivate."""

    def test_activate_module(self):
        module = make_module({"is_active": False})
        with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo:
            instance = MockRepo.return_value
            instance.get_by_id.return_value = module

            response = client.post(f"/modules/{MODULE_UUID}/activate")

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "activated"
        assert module.is_active is True
        mock_db.commit.assert_called()

    def test_activate_already_active_is_idempotent(self):
        module = make_module({"is_active": True})
        with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo:
            instance = MockRepo.return_value
            instance.get_by_id.return_value = module

            response = client.post(f"/modules/{MODULE_UUID}/activate")

        assert response.status_code == 200
        assert response.json()["status"] == "activated"

    def test_activate_not_found(self):
        with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo:
            instance = MockRepo.return_value
            instance.get_by_id.return_value = None
            instance.get_by_name.return_value = None

            response = client.post("/modules/nonexistent/activate")

        assert response.status_code == 404

    def test_deactivate_module(self):
        module = make_module({"is_active": True})
        with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo:
            instance = MockRepo.return_value
            instance.get_by_id.return_value = module

            response = client.post(f"/modules/{MODULE_UUID}/deactivate")

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "deactivated"
        assert module.is_active is False

    def test_deactivate_not_found(self):
        with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo:
            instance = MockRepo.return_value
            instance.get_by_id.return_value = None
            instance.get_by_name.return_value = None

            response = client.post("/modules/nonexistent/deactivate")

        assert response.status_code == 404


class TestSeedModules:
    """Tests for POST /modules/seed."""

    def test_seed_creates_modules(self):
        with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo, \
             patch("compliance.db.models.ServiceModuleDB") as MockSMDB, \
             patch("compliance.db.models.ModuleRegulationMappingDB") as MockMRMDB, \
             patch("compliance.db.models.ModuleRiskDB") as MockMRDB, \
             patch("classroom_engine.database.engine") as mock_engine:
            instance = MockRepo.return_value
            instance.seed_from_data.return_value = {
                "modules_created": 10,
                "mappings_created": 25,
            }
            # Prevent actual DB table creation
            MockSMDB.__table__ = MagicMock()
            MockMRMDB.__table__ = MagicMock()
            MockMRDB.__table__ = MagicMock()

            response = client.post("/modules/seed", json={"force": False})

        assert response.status_code == 200
        data = response.json()
        assert data["success"] is True
        assert data["modules_created"] == 10
        assert data["mappings_created"] == 25

    def test_seed_force_flag(self):
        """force=True is forwarded to seed_from_data."""
        with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo, \
             patch("compliance.db.models.ServiceModuleDB") as MockSMDB, \
             patch("compliance.db.models.ModuleRegulationMappingDB") as MockMRMDB, \
             patch("compliance.db.models.ModuleRiskDB") as MockMRDB, \
             patch("classroom_engine.database.engine"):
            instance = MockRepo.return_value
            instance.seed_from_data.return_value = {"modules_created": 0, "mappings_created": 0}
            MockSMDB.__table__ = MagicMock()
            MockMRMDB.__table__ = MagicMock()
            MockMRDB.__table__ = MagicMock()

            response = client.post("/modules/seed", json={"force": True})

        assert response.status_code == 200
        _, kwargs = instance.seed_from_data.call_args
        assert kwargs.get("force") is True


class TestRegulationMapping:
    """Tests for POST /modules/{id}/regulations."""

    def test_add_regulation_not_found_module(self):
        with patch("compliance.db.repository.ServiceModuleRepository") as MockModuleRepo, \
             patch("compliance.api.module_routes.RegulationRepository"):
            module_instance = MockModuleRepo.return_value
            module_instance.get_by_id.return_value = None
            module_instance.get_by_name.return_value = None

            response = client.post(
                f"/modules/{MODULE_UUID}/regulations",
                json={"module_id": MODULE_UUID, "regulation_id": REG_UUID, "relevance_level": "high"},
            )

        assert response.status_code == 404

    def test_add_regulation_not_found_regulation(self):
        module = make_module()
        with patch("compliance.db.repository.ServiceModuleRepository") as MockModuleRepo, \
             patch("compliance.api.module_routes.RegulationRepository") as MockRegRepo:
            module_instance = MockModuleRepo.return_value
            module_instance.get_by_id.return_value = module

            reg_instance = MockRegRepo.return_value
            reg_instance.get_by_id.return_value = None
            reg_instance.get_by_code.return_value = None

            response = client.post(
                f"/modules/{MODULE_UUID}/regulations",
                json={"module_id": MODULE_UUID, "regulation_id": "nonexistent-reg", "relevance_level": "high"},
            )

        assert response.status_code == 404

    def test_add_regulation_success(self):
        module = make_module()
        fake_regulation = MagicMock()
        fake_regulation.id = REG_UUID
        fake_regulation.code = "GDPR"
        fake_regulation.name = "DSGVO"

        fake_mapping = MagicMock()
        fake_mapping.id = "mapping-uuid"
        fake_mapping.module_id = MODULE_UUID
        fake_mapping.regulation_id = REG_UUID
        fake_mapping.relevance_level = MagicMock()
        fake_mapping.relevance_level.value = "high"
        fake_mapping.notes = None
        fake_mapping.applicable_articles = []
        fake_mapping.created_at = NOW

        with patch("compliance.db.repository.ServiceModuleRepository") as MockModuleRepo, \
             patch("compliance.api.module_routes.RegulationRepository") as MockRegRepo:
            module_instance = MockModuleRepo.return_value
            module_instance.get_by_id.return_value = module
            module_instance.add_regulation_mapping.return_value = fake_mapping

            reg_instance = MockRegRepo.return_value
            reg_instance.get_by_id.return_value = fake_regulation

            response = client.post(
                f"/modules/{MODULE_UUID}/regulations",
                json={"module_id": MODULE_UUID, "regulation_id": REG_UUID, "relevance_level": "high"},
            )

        assert response.status_code == 200
        data = response.json()
        assert data["relevance_level"] == "high"
        assert data["regulation_code"] == "GDPR"
