"""
Tests for TOM ↔ Canonical Control Mapping Routes.

Tests the three-layer architecture:
  TOM Measures → Mapping Bridge → Canonical Controls
"""

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

from compliance.api.tom_mapping_routes import (
    router,
    TOM_TO_CANONICAL_CATEGORIES,
    _compute_profile_hash,
)


# =============================================================================
# FIXTURES
# =============================================================================

@pytest.fixture
def app():
    """Create a test FastAPI app with the TOM mapping router."""
    from fastapi import FastAPI
    app = FastAPI()
    app.include_router(router)
    return app


@pytest.fixture
def client(app):
    return TestClient(app)


TENANT_ID = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"
PROJECT_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
HEADERS = {"X-Tenant-ID": TENANT_ID}


# =============================================================================
# UNIT TESTS
# =============================================================================

class TestCategoryMapping:
    """Test the TOM → Canonical category mapping dictionary."""

    def test_all_13_tom_categories_mapped(self):
        expected = {
            "ACCESS_CONTROL", "ADMISSION_CONTROL", "ACCESS_AUTHORIZATION",
            "TRANSFER_CONTROL", "INPUT_CONTROL", "ORDER_CONTROL",
            "AVAILABILITY", "SEPARATION", "ENCRYPTION", "PSEUDONYMIZATION",
            "RESILIENCE", "RECOVERY", "REVIEW",
        }
        assert set(TOM_TO_CANONICAL_CATEGORIES.keys()) == expected

    def test_each_category_has_at_least_one_canonical(self):
        for tom_cat, canonical_cats in TOM_TO_CANONICAL_CATEGORIES.items():
            assert len(canonical_cats) >= 1, f"{tom_cat} has no canonical categories"

    def test_canonical_categories_are_valid(self):
        """All referenced canonical categories must exist in the DB seed (migration 047)."""
        valid_canonical = {
            "encryption", "authentication", "network", "data_protection",
            "logging", "incident", "continuity", "compliance", "supply_chain",
            "physical", "personnel", "application", "system", "risk",
            "governance", "hardware", "identity",
        }
        for tom_cat, canonical_cats in TOM_TO_CANONICAL_CATEGORIES.items():
            for cc in canonical_cats:
                assert cc in valid_canonical, f"Invalid canonical category '{cc}' in {tom_cat}"


class TestProfileHash:
    """Test profile hash computation."""

    def test_same_input_same_hash(self):
        h1 = _compute_profile_hash("Telekommunikation", "medium")
        h2 = _compute_profile_hash("Telekommunikation", "medium")
        assert h1 == h2

    def test_different_input_different_hash(self):
        h1 = _compute_profile_hash("Telekommunikation", "medium")
        h2 = _compute_profile_hash("Gesundheitswesen", "large")
        assert h1 != h2

    def test_none_values_produce_hash(self):
        h = _compute_profile_hash(None, None)
        assert len(h) == 16

    def test_hash_is_16_chars(self):
        h = _compute_profile_hash("test", "small")
        assert len(h) == 16


# =============================================================================
# API ENDPOINT TESTS (with mocked DB)
# =============================================================================

class TestSyncEndpoint:
    """Test POST /tom-mappings/sync."""

    def test_sync_requires_tenant_header(self, client):
        resp = client.post("/tom-mappings/sync", json={"industry": "IT"})
        assert resp.status_code == 400
        assert "X-Tenant-ID" in resp.json()["detail"]

    @patch("compliance.api.tom_mapping_routes.SessionLocal")
    def test_sync_unchanged_profile_skips(self, mock_session_cls, client):
        """When profile hash matches, sync should return 'unchanged'."""
        mock_db = MagicMock()
        mock_session_cls.return_value.__enter__ = MagicMock(return_value=mock_db)
        mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)

        profile_hash = _compute_profile_hash("IT", "medium")
        mock_row = MagicMock()
        mock_row.profile_hash = profile_hash
        mock_db.execute.return_value.fetchone.return_value = mock_row

        resp = client.post(
            "/tom-mappings/sync",
            json={"industry": "IT", "company_size": "medium"},
            headers=HEADERS,
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "unchanged"

    @patch("compliance.api.tom_mapping_routes.SessionLocal")
    def test_sync_force_ignores_hash(self, mock_session_cls, client):
        """force=True should sync even if hash matches."""
        mock_db = MagicMock()
        mock_session_cls.return_value.__enter__ = MagicMock(return_value=mock_db)
        mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)

        # Return empty results for canonical control queries
        mock_db.execute.return_value.fetchall.return_value = []
        mock_db.execute.return_value.fetchone.return_value = None

        resp = client.post(
            "/tom-mappings/sync",
            json={"industry": "IT", "company_size": "medium", "force": True},
            headers=HEADERS,
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "synced"


class TestListEndpoint:
    """Test GET /tom-mappings."""

    def test_list_requires_tenant_header(self, client):
        resp = client.get("/tom-mappings")
        assert resp.status_code == 400

    @patch("compliance.api.tom_mapping_routes.SessionLocal")
    def test_list_returns_mappings(self, mock_session_cls, client):
        mock_db = MagicMock()
        mock_session_cls.return_value.__enter__ = MagicMock(return_value=mock_db)
        mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)

        mock_db.execute.return_value.fetchall.return_value = []
        mock_db.execute.return_value.scalar.return_value = 0

        resp = client.get("/tom-mappings", headers=HEADERS)
        assert resp.status_code == 200
        data = resp.json()
        assert "mappings" in data
        assert "total" in data


class TestByTomEndpoint:
    """Test GET /tom-mappings/by-tom/{code}."""

    def test_by_tom_requires_tenant_header(self, client):
        resp = client.get("/tom-mappings/by-tom/ENCRYPTION")
        assert resp.status_code == 400

    @patch("compliance.api.tom_mapping_routes.SessionLocal")
    def test_by_tom_returns_mappings(self, mock_session_cls, client):
        mock_db = MagicMock()
        mock_session_cls.return_value.__enter__ = MagicMock(return_value=mock_db)
        mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)

        mock_db.execute.return_value.fetchall.return_value = []

        resp = client.get("/tom-mappings/by-tom/ENCRYPTION", headers=HEADERS)
        assert resp.status_code == 200
        data = resp.json()
        assert data["tom_code"] == "ENCRYPTION"
        assert "mappings" in data


class TestStatsEndpoint:
    """Test GET /tom-mappings/stats."""

    def test_stats_requires_tenant_header(self, client):
        resp = client.get("/tom-mappings/stats")
        assert resp.status_code == 400

    @patch("compliance.api.tom_mapping_routes.SessionLocal")
    def test_stats_returns_structure(self, mock_session_cls, client):
        mock_db = MagicMock()
        mock_session_cls.return_value.__enter__ = MagicMock(return_value=mock_db)
        mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)

        mock_db.execute.return_value.fetchone.return_value = None
        mock_db.execute.return_value.fetchall.return_value = []
        mock_db.execute.return_value.scalar.return_value = 0

        resp = client.get("/tom-mappings/stats", headers=HEADERS)
        assert resp.status_code == 200
        data = resp.json()
        assert "sync_state" in data
        assert "category_breakdown" in data
        assert "total_canonical_controls_available" in data


class TestManualMappingEndpoint:
    """Test POST /tom-mappings/manual."""

    def test_manual_requires_tenant_header(self, client):
        resp = client.post("/tom-mappings/manual", json={
            "tom_control_code": "TOM-ENC-01",
            "tom_category": "ENCRYPTION",
            "canonical_control_id": str(uuid.uuid4()),
            "canonical_control_code": "CRYP-001",
        })
        assert resp.status_code == 400

    @patch("compliance.api.tom_mapping_routes.SessionLocal")
    def test_manual_404_if_canonical_not_found(self, mock_session_cls, client):
        mock_db = MagicMock()
        mock_session_cls.return_value.__enter__ = MagicMock(return_value=mock_db)
        mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)

        mock_db.execute.return_value.fetchone.return_value = None

        resp = client.post(
            "/tom-mappings/manual",
            json={
                "tom_control_code": "TOM-ENC-01",
                "tom_category": "ENCRYPTION",
                "canonical_control_id": str(uuid.uuid4()),
                "canonical_control_code": "CRYP-001",
            },
            headers=HEADERS,
        )
        assert resp.status_code == 404


class TestDeleteMappingEndpoint:
    """Test DELETE /tom-mappings/{id}."""

    def test_delete_requires_tenant_header(self, client):
        resp = client.delete(f"/tom-mappings/{uuid.uuid4()}")
        assert resp.status_code == 400

    @patch("compliance.api.tom_mapping_routes.SessionLocal")
    def test_delete_404_if_not_found(self, mock_session_cls, client):
        mock_db = MagicMock()
        mock_session_cls.return_value.__enter__ = MagicMock(return_value=mock_db)
        mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)

        mock_db.execute.return_value.rowcount = 0

        resp = client.delete(
            f"/tom-mappings/{uuid.uuid4()}",
            headers=HEADERS,
        )
        assert resp.status_code == 404
