"""Tests for extended dashboard routes (roadmap, module-status, next-actions, snapshots)."""

import pytest
from unittest.mock import MagicMock, patch
from fastapi.testclient import TestClient
from fastapi import FastAPI
from datetime import datetime, date, timedelta
from decimal import Decimal

from compliance.api.dashboard_routes import router
from classroom_engine.database import get_db
from compliance.api.tenant_utils import get_tenant_id

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


# =============================================================================
# Test App Setup
# =============================================================================

app = FastAPI()
app.include_router(router)

mock_db = MagicMock()


def override_get_db():
    yield mock_db


def override_tenant():
    return DEFAULT_TENANT_ID


app.dependency_overrides[get_db] = override_get_db
app.dependency_overrides[get_tenant_id] = override_tenant

client = TestClient(app)


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

class MockControl:
    def __init__(self, id="ctrl-001", control_id="CTRL-001", title="Test Control",
                 status_val="planned", domain_val="gov", owner="ISB",
                 next_review_at=None, category="security"):
        self.id = id
        self.control_id = control_id
        self.title = title
        self.status = MagicMock(value=status_val)
        self.domain = MagicMock(value=domain_val)
        self.owner = owner
        self.next_review_at = next_review_at
        self.category = category


class MockRisk:
    def __init__(self, inherent_risk_val="high", status="open"):
        self.inherent_risk = MagicMock(value=inherent_risk_val)
        self.status = status


def make_snapshot_row(overrides=None):
    data = {
        "id": "snap-001",
        "tenant_id": DEFAULT_TENANT_ID,
        "project_id": None,
        "score": Decimal("72.50"),
        "controls_total": 20,
        "controls_pass": 12,
        "controls_partial": 5,
        "evidence_total": 10,
        "evidence_valid": 8,
        "risks_total": 5,
        "risks_high": 2,
        "snapshot_date": date(2026, 3, 14),
        "created_at": datetime(2026, 3, 14),
    }
    if overrides:
        data.update(overrides)
    row = MagicMock()
    row._mapping = data
    return row


# =============================================================================
# Tests
# =============================================================================

class TestDashboardRoadmap:
    def test_roadmap_returns_buckets(self):
        """Roadmap returns 4 buckets with controls."""
        overdue = datetime.utcnow() - timedelta(days=10)
        future = datetime.utcnow() + timedelta(days=30)

        with patch("compliance.api.dashboard_routes.ControlRepository") as MockCtrlRepo:
            instance = MockCtrlRepo.return_value
            instance.get_all.return_value = [
                MockControl(id="c1", status_val="planned", category="legal", next_review_at=overdue),
                MockControl(id="c2", status_val="partial", category="security"),
                MockControl(id="c3", status_val="planned", category="best_practice"),
                MockControl(id="c4", status_val="pass"),  # should be excluded
            ]

            resp = client.get("/dashboard/roadmap")
            assert resp.status_code == 200
            data = resp.json()
            assert "buckets" in data
            assert "counts" in data
            # c4 is pass, so excluded; c1 is legal+overdue → quick_wins
            total_in_buckets = sum(data["counts"].values())
            assert total_in_buckets == 3

    def test_roadmap_empty_controls(self):
        """Roadmap with no controls returns empty buckets."""
        with patch("compliance.api.dashboard_routes.ControlRepository") as MockCtrlRepo:
            MockCtrlRepo.return_value.get_all.return_value = []
            resp = client.get("/dashboard/roadmap")
            assert resp.status_code == 200
            assert all(v == 0 for v in resp.json()["counts"].values())


class TestModuleStatus:
    def test_module_status_returns_modules(self):
        """Module status returns list of modules with counts."""
        # Mock db.execute for each module's COUNT query
        count_result = MagicMock()
        count_result.fetchone.return_value = (5,)
        mock_db.execute.return_value = count_result

        resp = client.get("/dashboard/module-status")
        assert resp.status_code == 200
        data = resp.json()
        assert "modules" in data
        assert data["total"] > 0
        assert all(m["count"] == 5 for m in data["modules"])

    def test_module_status_handles_missing_tables(self):
        """Module status handles missing tables gracefully."""
        mock_db.execute.side_effect = Exception("relation does not exist")

        resp = client.get("/dashboard/module-status")
        assert resp.status_code == 200
        data = resp.json()
        # All modules should have count=0 and status=not_started
        assert all(m["count"] == 0 for m in data["modules"])
        assert all(m["status"] == "not_started" for m in data["modules"])

        mock_db.execute.side_effect = None  # reset


class TestNextActions:
    def test_next_actions_returns_sorted(self):
        """Next actions returns controls sorted by urgency."""
        overdue = datetime.utcnow() - timedelta(days=30)

        with patch("compliance.api.dashboard_routes.ControlRepository") as MockCtrlRepo:
            instance = MockCtrlRepo.return_value
            instance.get_all.return_value = [
                MockControl(id="c1", status_val="planned", category="legal", next_review_at=overdue),
                MockControl(id="c2", status_val="partial", category="best_practice"),
                MockControl(id="c3", status_val="pass"),  # excluded
            ]

            resp = client.get("/dashboard/next-actions?limit=5")
            assert resp.status_code == 200
            data = resp.json()
            assert len(data["actions"]) == 2
            # c1 should be first (higher urgency due to legal + overdue)
            assert data["actions"][0]["control_id"] == "CTRL-001"


class TestScoreSnapshot:
    def test_create_snapshot(self):
        """Creating a snapshot saves current score."""
        with patch("compliance.api.dashboard_routes.ControlRepository") as MockCtrlRepo, \
             patch("compliance.api.dashboard_routes.EvidenceRepository") as MockEvRepo, \
             patch("compliance.api.dashboard_routes.RiskRepository") as MockRiskRepo:

            MockCtrlRepo.return_value.get_statistics.return_value = {
                "total": 20, "pass": 12, "partial": 5, "by_status": {}
            }
            MockEvRepo.return_value.get_statistics.return_value = {
                "total": 10, "by_status": {"valid": 8}
            }
            MockRiskRepo.return_value.get_all.return_value = [
                MockRisk("high"), MockRisk("critical"), MockRisk("low")
            ]

            snap_row = make_snapshot_row()
            mock_db.execute.return_value.fetchone.return_value = snap_row

            resp = client.post("/dashboard/snapshot")
            assert resp.status_code == 200
            data = resp.json()
            assert "score" in data

    def test_score_history(self):
        """Score history returns snapshots."""
        rows = [make_snapshot_row({"snapshot_date": date(2026, 3, i)}) for i in range(1, 4)]
        mock_db.execute.return_value.fetchall.return_value = rows

        resp = client.get("/dashboard/score-history?months=3")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 3
        assert len(data["snapshots"]) == 3
