"""Tests for AI Quality Metrics and Tests routes (quality_routes.py).

Covers:
- Schema validation (MetricCreate, MetricUpdate, TestCreate, TestUpdate)
- Helper functions (_row_to_dict, _get_tenant_id)
- HTTP endpoints via FastAPI TestClient with mocked DB session
"""

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

from compliance.api.quality_routes import (
    router,
    MetricCreate,
    MetricUpdate,
    TestCreate,
    TestUpdate,
)
from compliance.api.db_utils import row_to_dict as _row_to_dict

DEFAULT_TENANT_ID = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"
from classroom_engine.database import get_db

# =============================================================================
# TestClient Setup
# =============================================================================

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

DEFAULT_TENANT = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"
OTHER_TENANT = "bbbbbbbb-cccc-dddd-eeee-ffffffffffff"
METRIC_ID = "bbbbbbbb-0001-0001-0001-000000000001"
TEST_ID = "cccccccc-0001-0001-0001-000000000001"


def make_metric_row(overrides=None):
    data = {
        "id": METRIC_ID,
        "tenant_id": DEFAULT_TENANT,
        "name": "Test Metric",
        "category": "accuracy",
        "score": 85.0,
        "threshold": 80.0,
        "trend": "stable",
        "ai_system": None,
        "last_measured": datetime(2024, 1, 1),
        "created_at": datetime(2024, 1, 1),
        "updated_at": datetime(2024, 1, 1),
    }
    if overrides:
        data.update(overrides)
    row = MagicMock()
    row._mapping = data
    return row


def make_test_row(overrides=None):
    data = {
        "id": TEST_ID,
        "tenant_id": DEFAULT_TENANT,
        "name": "Test Run",
        "status": "passed",
        "duration": "1.2s",
        "ai_system": None,
        "details": None,
        "last_run": datetime(2024, 1, 1),
        "created_at": datetime(2024, 1, 1),
        "updated_at": datetime(2024, 1, 1),
    }
    if overrides:
        data.update(overrides)
    row = MagicMock()
    row._mapping = data
    return row


@pytest.fixture
def mock_db():
    db = MagicMock()

    def override_get_db():
        yield db

    app.dependency_overrides[get_db] = override_get_db
    yield db
    app.dependency_overrides.clear()


# =============================================================================
# Schema Tests — MetricCreate
# =============================================================================

class TestMetricCreate:
    def test_minimal_valid(self):
        req = MetricCreate(name="Accuracy Score")
        assert req.name == "Accuracy Score"
        assert req.category == "accuracy"
        assert req.score == 0.0
        assert req.threshold == 80.0
        assert req.trend == "stable"
        assert req.ai_system is None
        assert req.last_measured is None

    def test_full_values(self):
        ts = datetime(2026, 3, 1)
        req = MetricCreate(
            name="Fairness Score",
            category="fairness",
            score=92.5,
            threshold=85.0,
            trend="improving",
            ai_system="RecSys-v2",
            last_measured=ts,
        )
        assert req.name == "Fairness Score"
        assert req.category == "fairness"
        assert req.score == 92.5
        assert req.trend == "improving"
        assert req.ai_system == "RecSys-v2"
        assert req.last_measured == ts

    def test_serialization_excludes_none(self):
        req = MetricCreate(name="Drift Score", score=75.0)
        data = req.model_dump(exclude_none=True)
        assert data["name"] == "Drift Score"
        assert data["score"] == 75.0
        assert "ai_system" not in data
        assert "last_measured" not in data

    def test_default_trend_stable(self):
        req = MetricCreate(name="Test")
        assert req.trend == "stable"

    def test_default_score_zero(self):
        req = MetricCreate(name="Test")
        assert req.score == 0.0


# =============================================================================
# Schema Tests — MetricUpdate
# =============================================================================

class TestMetricUpdate:
    def test_empty_update(self):
        req = MetricUpdate()
        data = req.model_dump(exclude_unset=True)
        assert data == {}

    def test_partial_score_update(self):
        req = MetricUpdate(score=95.0)
        data = req.model_dump(exclude_unset=True)
        assert data == {"score": 95.0}

    def test_trend_and_threshold_update(self):
        req = MetricUpdate(trend="declining", threshold=90.0)
        data = req.model_dump(exclude_unset=True)
        assert data["trend"] == "declining"
        assert data["threshold"] == 90.0
        assert "name" not in data

    def test_full_update(self):
        req = MetricUpdate(
            name="New Name",
            category="robustness",
            score=88.0,
            threshold=85.0,
            trend="improving",
            ai_system="ModelB",
        )
        data = req.model_dump(exclude_unset=True)
        assert len(data) == 6
        assert data["category"] == "robustness"


# =============================================================================
# Schema Tests — TestCreate
# =============================================================================

class TestTestCreate:
    def test_minimal_valid(self):
        req = TestCreate(name="Bias Detection Test")
        assert req.name == "Bias Detection Test"
        assert req.status == "pending"
        assert req.duration is None
        assert req.ai_system is None
        assert req.details is None
        assert req.last_run is None

    def test_full_values(self):
        ts = datetime(2026, 3, 1, 12, 0, 0)
        req = TestCreate(
            name="Accuracy Test Suite",
            status="passed",
            duration="3.45s",
            ai_system="ClassifierV3",
            details="All 500 samples passed",
            last_run=ts,
        )
        assert req.status == "passed"
        assert req.duration == "3.45s"
        assert req.ai_system == "ClassifierV3"
        assert req.details == "All 500 samples passed"
        assert req.last_run == ts

    def test_failed_status(self):
        req = TestCreate(name="Fairness Check", status="failed")
        assert req.status == "failed"

    def test_serialization_excludes_none(self):
        req = TestCreate(name="Quick Test", status="passed")
        data = req.model_dump(exclude_none=True)
        assert "duration" not in data
        assert "ai_system" not in data


# =============================================================================
# Schema Tests — TestUpdate
# =============================================================================

class TestTestUpdate:
    def test_empty_update(self):
        req = TestUpdate()
        data = req.model_dump(exclude_unset=True)
        assert data == {}

    def test_status_update(self):
        req = TestUpdate(status="failed")
        data = req.model_dump(exclude_unset=True)
        assert data == {"status": "failed"}

    def test_duration_and_details_update(self):
        req = TestUpdate(duration="10.5s", details="Timeout on 3 samples")
        data = req.model_dump(exclude_unset=True)
        assert data["duration"] == "10.5s"
        assert data["details"] == "Timeout on 3 samples"
        assert "name" not in data


# =============================================================================
# Helper Tests — _row_to_dict
# =============================================================================

class TestRowToDict:
    def test_basic_metric_conversion(self):
        row = make_metric_row()
        result = _row_to_dict(row)
        assert result["id"] == METRIC_ID
        assert result["name"] == "Test Metric"
        assert result["score"] == 85.0
        assert result["threshold"] == 80.0

    def test_datetime_serialized(self):
        ts = datetime(2024, 6, 15, 9, 0, 0)
        row = make_metric_row({"created_at": ts, "last_measured": ts})
        result = _row_to_dict(row)
        assert result["created_at"] == ts.isoformat()
        assert result["last_measured"] == ts.isoformat()

    def test_none_values_preserved(self):
        row = make_metric_row()
        result = _row_to_dict(row)
        assert result["ai_system"] is None

    def test_uuid_converted_to_string(self):
        import uuid
        uid = uuid.UUID(DEFAULT_TENANT)
        row = MagicMock()
        row._mapping = {"id": uid, "tenant_id": uid}
        result = _row_to_dict(row)
        assert result["id"] == str(uid)

    def test_numeric_fields_unchanged(self):
        row = MagicMock()
        row._mapping = {"score": 92.5, "threshold": 80.0, "count": 10}
        result = _row_to_dict(row)
        assert result["score"] == 92.5
        assert result["threshold"] == 80.0
        assert result["count"] == 10



# =============================================================================
# HTTP Tests — GET /quality/stats
# =============================================================================

class TestGetQualityStats:
    def test_stats_all_zeros(self, mock_db):
        metrics_row = MagicMock()
        metrics_row.total_metrics = 0
        metrics_row.avg_score = 0
        metrics_row.metrics_above_threshold = 0

        tests_row = MagicMock()
        tests_row.passed = 0
        tests_row.failed = 0
        tests_row.warning = 0
        tests_row.total = 0

        mock_db.execute.return_value.fetchone.side_effect = [metrics_row, tests_row]

        response = client.get("/quality/stats")
        assert response.status_code == 200
        data = response.json()
        assert data["total_metrics"] == 0
        assert data["avg_score"] == 0.0
        assert data["metrics_above_threshold"] == 0
        assert data["passed"] == 0
        assert data["failed"] == 0
        assert data["warning"] == 0
        assert data["total_tests"] == 0

    def test_stats_with_data(self, mock_db):
        metrics_row = MagicMock()
        metrics_row.total_metrics = 5
        metrics_row.avg_score = 87.4
        metrics_row.metrics_above_threshold = 4

        tests_row = MagicMock()
        tests_row.passed = 10
        tests_row.failed = 2
        tests_row.warning = 1
        tests_row.total = 13

        mock_db.execute.return_value.fetchone.side_effect = [metrics_row, tests_row]

        response = client.get("/quality/stats")
        assert response.status_code == 200
        data = response.json()
        assert data["total_metrics"] == 5
        assert data["avg_score"] == 87.4
        assert data["metrics_above_threshold"] == 4
        assert data["passed"] == 10
        assert data["failed"] == 2
        assert data["warning"] == 1
        assert data["total_tests"] == 13

    def test_stats_none_values_become_zero(self, mock_db):
        metrics_row = MagicMock()
        metrics_row.total_metrics = None
        metrics_row.avg_score = None
        metrics_row.metrics_above_threshold = None

        tests_row = MagicMock()
        tests_row.passed = None
        tests_row.failed = None
        tests_row.warning = None
        tests_row.total = None

        mock_db.execute.return_value.fetchone.side_effect = [metrics_row, tests_row]

        response = client.get("/quality/stats")
        assert response.status_code == 200
        data = response.json()
        assert data["total_metrics"] == 0
        assert data["avg_score"] == 0.0
        assert data["total_tests"] == 0

    def test_stats_with_tenant_header(self, mock_db):
        metrics_row = MagicMock()
        metrics_row.total_metrics = 2
        metrics_row.avg_score = 90.0
        metrics_row.metrics_above_threshold = 2

        tests_row = MagicMock()
        tests_row.passed = 5
        tests_row.failed = 0
        tests_row.warning = 0
        tests_row.total = 5

        mock_db.execute.return_value.fetchone.side_effect = [metrics_row, tests_row]

        response = client.get(
            "/quality/stats",
            headers={"X-Tenant-Id": OTHER_TENANT},
        )
        assert response.status_code == 200
        assert response.json()["total_metrics"] == 2


# =============================================================================
# HTTP Tests — GET /quality/metrics
# =============================================================================

class TestListMetrics:
    def test_empty_list(self, mock_db):
        count_row = MagicMock()
        count_row.__getitem__ = lambda self, i: 0
        execute_result = MagicMock()
        execute_result.fetchone.return_value = count_row
        execute_result.fetchall.return_value = []
        mock_db.execute.return_value = execute_result

        response = client.get("/quality/metrics")
        assert response.status_code == 200
        data = response.json()
        assert data["metrics"] == []
        assert data["total"] == 0

    def test_list_with_data(self, mock_db):
        count_row = MagicMock()
        count_row.__getitem__ = lambda self, i: 2
        m1 = make_metric_row()
        m2 = make_metric_row({"id": "bbbbbbbb-0002-0002-0002-000000000002", "name": "Second Metric"})

        execute_result = MagicMock()
        execute_result.fetchone.return_value = count_row
        execute_result.fetchall.return_value = [m1, m2]
        mock_db.execute.return_value = execute_result

        response = client.get("/quality/metrics")
        assert response.status_code == 200
        data = response.json()
        assert len(data["metrics"]) == 2
        assert data["total"] == 2

    def test_filter_by_category(self, mock_db):
        count_row = MagicMock()
        count_row.__getitem__ = lambda self, i: 1
        execute_result = MagicMock()
        execute_result.fetchone.return_value = count_row
        execute_result.fetchall.return_value = [make_metric_row({"category": "fairness"})]
        mock_db.execute.return_value = execute_result

        response = client.get("/quality/metrics?category=fairness")
        assert response.status_code == 200
        data = response.json()
        assert data["metrics"][0]["category"] == "fairness"

    def test_filter_by_ai_system(self, mock_db):
        count_row = MagicMock()
        count_row.__getitem__ = lambda self, i: 1
        execute_result = MagicMock()
        execute_result.fetchone.return_value = count_row
        execute_result.fetchall.return_value = [make_metric_row({"ai_system": "ModelAlpha"})]
        mock_db.execute.return_value = execute_result

        response = client.get("/quality/metrics?ai_system=ModelAlpha")
        assert response.status_code == 200
        data = response.json()
        assert data["metrics"][0]["ai_system"] == "ModelAlpha"

    def test_pagination(self, mock_db):
        count_row = MagicMock()
        count_row.__getitem__ = lambda self, i: 20
        execute_result = MagicMock()
        execute_result.fetchone.return_value = count_row
        execute_result.fetchall.return_value = [make_metric_row()]
        mock_db.execute.return_value = execute_result

        response = client.get("/quality/metrics?limit=1&offset=10")
        assert response.status_code == 200
        data = response.json()
        assert data["total"] == 20
        assert len(data["metrics"]) == 1


# =============================================================================
# HTTP Tests — POST /quality/metrics
# =============================================================================

class TestCreateMetric:
    def test_create_success(self, mock_db):
        created_row = make_metric_row({"name": "New Metric"})
        mock_db.execute.return_value.fetchone.return_value = created_row

        response = client.post(
            "/quality/metrics",
            json={"name": "New Metric"},
        )
        assert response.status_code == 201
        data = response.json()
        assert data["name"] == "New Metric"
        mock_db.commit.assert_called_once()

    def test_create_full_metric(self, mock_db):
        created_row = make_metric_row({
            "name": "Robustness Score",
            "category": "robustness",
            "score": 78.5,
            "threshold": 75.0,
            "trend": "improving",
        })
        mock_db.execute.return_value.fetchone.return_value = created_row

        response = client.post("/quality/metrics", json={
            "name": "Robustness Score",
            "category": "robustness",
            "score": 78.5,
            "threshold": 75.0,
            "trend": "improving",
        })
        assert response.status_code == 201
        data = response.json()
        assert data["category"] == "robustness"
        assert data["score"] == 78.5

    def test_create_missing_name_fails(self, mock_db):
        response = client.post("/quality/metrics", json={"score": 90.0})
        assert response.status_code == 422

    def test_create_with_tenant_header(self, mock_db):
        created_row = make_metric_row({"tenant_id": OTHER_TENANT})
        mock_db.execute.return_value.fetchone.return_value = created_row

        response = client.post(
            "/quality/metrics",
            json={"name": "Tenant B metric"},
            headers={"X-Tenant-Id": OTHER_TENANT},
        )
        assert response.status_code == 201
        assert mock_db.commit.called


# =============================================================================
# HTTP Tests — PUT /quality/metrics/{id}
# =============================================================================

class TestUpdateMetric:
    def test_update_success(self, mock_db):
        updated_row = make_metric_row({"score": 95.0, "trend": "improving"})
        mock_db.execute.return_value.fetchone.return_value = updated_row

        response = client.put(
            f"/quality/metrics/{METRIC_ID}",
            json={"score": 95.0, "trend": "improving"},
        )
        assert response.status_code == 200
        data = response.json()
        assert data["score"] == 95.0
        assert data["trend"] == "improving"
        mock_db.commit.assert_called_once()

    def test_update_not_found_returns_404(self, mock_db):
        mock_db.execute.return_value.fetchone.return_value = None

        response = client.put(
            "/quality/metrics/nonexistent-id",
            json={"score": 50.0},
        )
        assert response.status_code == 404
        assert "not found" in response.json()["detail"].lower()

    def test_update_no_fields_returns_400(self, mock_db):
        response = client.put(f"/quality/metrics/{METRIC_ID}", json={})
        assert response.status_code == 400

    def test_partial_update_category(self, mock_db):
        updated_row = make_metric_row({"category": "explainability"})
        mock_db.execute.return_value.fetchone.return_value = updated_row

        response = client.put(
            f"/quality/metrics/{METRIC_ID}",
            json={"category": "explainability"},
        )
        assert response.status_code == 200
        assert response.json()["category"] == "explainability"


# =============================================================================
# HTTP Tests — DELETE /quality/metrics/{id}
# =============================================================================

class TestDeleteMetric:
    def test_delete_success_204(self, mock_db):
        delete_result = MagicMock()
        delete_result.rowcount = 1
        mock_db.execute.return_value = delete_result

        response = client.delete(f"/quality/metrics/{METRIC_ID}")
        assert response.status_code == 204
        mock_db.commit.assert_called_once()

    def test_delete_not_found_returns_404(self, mock_db):
        delete_result = MagicMock()
        delete_result.rowcount = 0
        mock_db.execute.return_value = delete_result

        response = client.delete("/quality/metrics/nonexistent-id")
        assert response.status_code == 404
        assert "not found" in response.json()["detail"].lower()

    def test_delete_with_tenant_header(self, mock_db):
        delete_result = MagicMock()
        delete_result.rowcount = 1
        mock_db.execute.return_value = delete_result

        response = client.delete(
            f"/quality/metrics/{METRIC_ID}",
            headers={"X-Tenant-Id": OTHER_TENANT},
        )
        assert response.status_code == 204


# =============================================================================
# HTTP Tests — GET /quality/tests
# =============================================================================

class TestListQualityTests:
    def test_empty_list(self, mock_db):
        count_row = MagicMock()
        count_row.__getitem__ = lambda self, i: 0
        execute_result = MagicMock()
        execute_result.fetchone.return_value = count_row
        execute_result.fetchall.return_value = []
        mock_db.execute.return_value = execute_result

        response = client.get("/quality/tests")
        assert response.status_code == 200
        data = response.json()
        assert data["tests"] == []
        assert data["total"] == 0

    def test_list_with_data(self, mock_db):
        count_row = MagicMock()
        count_row.__getitem__ = lambda self, i: 2
        t1 = make_test_row()
        t2 = make_test_row({"id": "cccccccc-0002-0002-0002-000000000002", "name": "Second Test"})

        execute_result = MagicMock()
        execute_result.fetchone.return_value = count_row
        execute_result.fetchall.return_value = [t1, t2]
        mock_db.execute.return_value = execute_result

        response = client.get("/quality/tests")
        assert response.status_code == 200
        data = response.json()
        assert len(data["tests"]) == 2
        assert data["total"] == 2

    def test_filter_by_status(self, mock_db):
        count_row = MagicMock()
        count_row.__getitem__ = lambda self, i: 1
        execute_result = MagicMock()
        execute_result.fetchone.return_value = count_row
        execute_result.fetchall.return_value = [make_test_row({"status": "failed"})]
        mock_db.execute.return_value = execute_result

        response = client.get("/quality/tests?status=failed")
        assert response.status_code == 200
        data = response.json()
        assert data["tests"][0]["status"] == "failed"

    def test_filter_by_ai_system(self, mock_db):
        count_row = MagicMock()
        count_row.__getitem__ = lambda self, i: 1
        execute_result = MagicMock()
        execute_result.fetchone.return_value = count_row
        execute_result.fetchall.return_value = [make_test_row({"ai_system": "ModelBeta"})]
        mock_db.execute.return_value = execute_result

        response = client.get("/quality/tests?ai_system=ModelBeta")
        assert response.status_code == 200
        data = response.json()
        assert data["tests"][0]["ai_system"] == "ModelBeta"

    def test_pagination(self, mock_db):
        count_row = MagicMock()
        count_row.__getitem__ = lambda self, i: 50
        execute_result = MagicMock()
        execute_result.fetchone.return_value = count_row
        execute_result.fetchall.return_value = [make_test_row()]
        mock_db.execute.return_value = execute_result

        response = client.get("/quality/tests?limit=1&offset=5")
        assert response.status_code == 200
        assert response.json()["total"] == 50


# =============================================================================
# HTTP Tests — POST /quality/tests
# =============================================================================

class TestCreateQualityTest:
    def test_create_success(self, mock_db):
        created_row = make_test_row({"name": "New Test Run"})
        mock_db.execute.return_value.fetchone.return_value = created_row

        response = client.post(
            "/quality/tests",
            json={"name": "New Test Run"},
        )
        assert response.status_code == 201
        data = response.json()
        assert data["name"] == "New Test Run"
        mock_db.commit.assert_called_once()

    def test_create_full_test(self, mock_db):
        created_row = make_test_row({
            "name": "Full Test Suite",
            "status": "passed",
            "duration": "5.0s",
            "ai_system": "Classifier",
            "details": "All assertions passed",
        })
        mock_db.execute.return_value.fetchone.return_value = created_row

        response = client.post("/quality/tests", json={
            "name": "Full Test Suite",
            "status": "passed",
            "duration": "5.0s",
            "ai_system": "Classifier",
            "details": "All assertions passed",
        })
        assert response.status_code == 201
        data = response.json()
        assert data["status"] == "passed"
        assert data["duration"] == "5.0s"

    def test_create_missing_name_fails(self, mock_db):
        response = client.post("/quality/tests", json={"status": "passed"})
        assert response.status_code == 422

    def test_create_failed_status(self, mock_db):
        created_row = make_test_row({"status": "failed"})
        mock_db.execute.return_value.fetchone.return_value = created_row

        response = client.post(
            "/quality/tests",
            json={"name": "Failing test", "status": "failed"},
        )
        assert response.status_code == 201
        assert response.json()["status"] == "failed"


# =============================================================================
# HTTP Tests — PUT /quality/tests/{id}
# =============================================================================

class TestUpdateQualityTest:
    def test_update_success(self, mock_db):
        updated_row = make_test_row({"status": "failed", "details": "Assertion error on line 42"})
        mock_db.execute.return_value.fetchone.return_value = updated_row

        response = client.put(
            f"/quality/tests/{TEST_ID}",
            json={"status": "failed", "details": "Assertion error on line 42"},
        )
        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "failed"
        assert data["details"] == "Assertion error on line 42"
        mock_db.commit.assert_called_once()

    def test_update_not_found_returns_404(self, mock_db):
        mock_db.execute.return_value.fetchone.return_value = None

        response = client.put(
            "/quality/tests/nonexistent-id",
            json={"status": "passed"},
        )
        assert response.status_code == 404
        assert "not found" in response.json()["detail"].lower()

    def test_update_no_fields_returns_400(self, mock_db):
        response = client.put(f"/quality/tests/{TEST_ID}", json={})
        assert response.status_code == 400

    def test_update_duration_only(self, mock_db):
        updated_row = make_test_row({"duration": "2.8s"})
        mock_db.execute.return_value.fetchone.return_value = updated_row

        response = client.put(
            f"/quality/tests/{TEST_ID}",
            json={"duration": "2.8s"},
        )
        assert response.status_code == 200
        assert response.json()["duration"] == "2.8s"

    def test_update_with_tenant_header(self, mock_db):
        updated_row = make_test_row({"tenant_id": OTHER_TENANT, "status": "warning"})
        mock_db.execute.return_value.fetchone.return_value = updated_row

        response = client.put(
            f"/quality/tests/{TEST_ID}",
            json={"status": "warning"},
            headers={"X-Tenant-Id": OTHER_TENANT},
        )
        assert response.status_code == 200
        assert response.json()["status"] == "warning"


# =============================================================================
# HTTP Tests — DELETE /quality/tests/{id}
# =============================================================================

class TestDeleteQualityTest:
    def test_delete_success_204(self, mock_db):
        delete_result = MagicMock()
        delete_result.rowcount = 1
        mock_db.execute.return_value = delete_result

        response = client.delete(f"/quality/tests/{TEST_ID}")
        assert response.status_code == 204
        mock_db.commit.assert_called_once()

    def test_delete_not_found_returns_404(self, mock_db):
        delete_result = MagicMock()
        delete_result.rowcount = 0
        mock_db.execute.return_value = delete_result

        response = client.delete("/quality/tests/nonexistent-id")
        assert response.status_code == 404
        assert "not found" in response.json()["detail"].lower()

    def test_delete_with_tenant_header(self, mock_db):
        delete_result = MagicMock()
        delete_result.rowcount = 1
        mock_db.execute.return_value = delete_result

        response = client.delete(
            f"/quality/tests/{TEST_ID}",
            headers={"X-Tenant-Id": OTHER_TENANT},
        )
        assert response.status_code == 204
        assert mock_db.commit.called


# =============================================================================
# Tenant Isolation Tests
# =============================================================================

class TestTenantIsolation:
    def test_metrics_tenant_isolation(self, mock_db):
        """Tenant A sees 3 metrics, Tenant B sees 0."""
        def side_effect(query, params):
            result = MagicMock()
            tid = params.get("tenant_id", "")
            if tid == DEFAULT_TENANT:
                count_row = MagicMock()
                count_row.__getitem__ = lambda self, i: 3
                result.fetchone.return_value = count_row
                result.fetchall.return_value = [
                    make_metric_row(),
                    make_metric_row({"id": "bbbbbbbb-0002-0002-0002-000000000002"}),
                    make_metric_row({"id": "bbbbbbbb-0003-0003-0003-000000000003"}),
                ]
            else:
                count_row = MagicMock()
                count_row.__getitem__ = lambda self, i: 0
                result.fetchone.return_value = count_row
                result.fetchall.return_value = []
            return result

        mock_db.execute.side_effect = side_effect

        resp_a = client.get(
            "/quality/metrics",
            headers={"X-Tenant-Id": DEFAULT_TENANT},
        )
        assert resp_a.json()["total"] == 3

        resp_b = client.get(
            "/quality/metrics",
            headers={"X-Tenant-Id": OTHER_TENANT},
        )
        assert resp_b.json()["total"] == 0

    def test_tests_tenant_isolation(self, mock_db):
        """Tenant A sees 2 tests, Tenant B sees 0."""
        def side_effect(query, params):
            result = MagicMock()
            tid = params.get("tenant_id", "")
            if tid == DEFAULT_TENANT:
                count_row = MagicMock()
                count_row.__getitem__ = lambda self, i: 2
                result.fetchone.return_value = count_row
                result.fetchall.return_value = [make_test_row(), make_test_row({"id": "cccccccc-0002-0002-0002-000000000002"})]
            else:
                count_row = MagicMock()
                count_row.__getitem__ = lambda self, i: 0
                result.fetchone.return_value = count_row
                result.fetchall.return_value = []
            return result

        mock_db.execute.side_effect = side_effect

        resp_a = client.get("/quality/tests", headers={"X-Tenant-Id": DEFAULT_TENANT})
        assert resp_a.json()["total"] == 2

        resp_b = client.get("/quality/tests", headers={"X-Tenant-Id": OTHER_TENANT})
        assert resp_b.json()["total"] == 0

    def test_invalid_tenant_header_returns_400(self, mock_db):
        response = client.get(
            "/quality/metrics",
            headers={"X-Tenant-Id": "bad-uuid"},
        )
        assert response.status_code == 400

    def test_delete_wrong_tenant_returns_404(self, mock_db):
        """Deleting a metric that belongs to a different tenant returns 404."""
        delete_result = MagicMock()
        delete_result.rowcount = 0
        mock_db.execute.return_value = delete_result

        response = client.delete(
            f"/quality/metrics/{METRIC_ID}",
            headers={"X-Tenant-Id": OTHER_TENANT},
        )
        assert response.status_code == 404
