"""Tests for Prometheus instrumentation of the TextIQ DE Airflow plugin.

Covers the I/O matrix from the spec:
- Successful upload increments upload_total{outcome="success"}
- Validation-failed upload increments upload_total{outcome="client_error"}
- HMAC failure: upload_total NOT incremented; hmac_verify_duration_seconds IS recorded
- /metrics per sub-app returns correct Content-Type and metric names

sys.path injection matches the pattern used in test_middleware.py / test_tags.py.
"""

import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

_plugins_dir = str(Path(__file__).resolve().parents[2] / "airflow" / "plugins")
if _plugins_dir not in sys.path:
    sys.path.insert(0, _plugins_dir)

# Import metrics module first so counters are registered
import api.metrics as metrics_mod
from api.metrics import (
    approve_total,
    delete_total,
    domain_term_endpoint_duration_seconds,
    domain_term_op_total,
    hmac_verify_duration_seconds,
    metrics_response,
    outcome_for,
    record_outcome,
    register_metrics_route,
    reject_total,
    tag_endpoint_duration_seconds,
    tag_op_total,
    upload_duration_seconds,
    upload_total,
)
from prometheus_client import Counter, Histogram


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

def _get_counter_value(counter: Counter, labels: dict) -> float:
    """Return current total for a counter label set via .collect() (prometheus_client 0.25+ API)."""
    name = counter._name + "_total"
    for family in counter.collect():
        for sample in family.samples:
            if sample.name == name and sample.labels == labels:
                return sample.value
    return 0.0


def _get_histogram_count(histogram: Histogram, labels: dict) -> float:
    """Return current _count for a histogram label set via .collect()."""
    name = histogram._name + "_count"
    for family in histogram.collect():
        for sample in family.samples:
            if sample.name == name and sample.labels == labels:
                return sample.value
    return 0.0


# ---------------------------------------------------------------------------
# outcome_for helper
# ---------------------------------------------------------------------------

class TestOutcomeFor:
    def test_2xx_is_success(self):
        assert outcome_for(200) == "success"
        assert outcome_for(201) == "success"
        assert outcome_for(202) == "success"

    def test_4xx_is_client_error(self):
        assert outcome_for(400) == "client_error"
        assert outcome_for(404) == "client_error"
        assert outcome_for(422) == "client_error"

    def test_5xx_is_server_error(self):
        assert outcome_for(500) == "server_error"
        assert outcome_for(503) == "server_error"

    def test_204_is_success(self):
        assert outcome_for(204) == "success"


# ---------------------------------------------------------------------------
# record_outcome helper
# ---------------------------------------------------------------------------

class TestRecordOutcome:
    def test_increments_success_label(self):
        counter = Counter(
            "test_record_outcome_success_total",
            "Test counter",
            ["outcome", "tenant_id"],
        )
        before = _get_counter_value(counter, {"outcome": "success", "tenant_id": "t-test"})
        record_outcome(counter, 200, tenant_id="t-test")
        after = _get_counter_value(counter, {"outcome": "success", "tenant_id": "t-test"})
        assert after - before == 1.0

    def test_increments_client_error_label(self):
        counter = Counter(
            "test_record_outcome_client_error_total",
            "Test counter",
            ["outcome", "tenant_id"],
        )
        before = _get_counter_value(counter, {"outcome": "client_error", "tenant_id": "t-test"})
        record_outcome(counter, 400, tenant_id="t-test")
        after = _get_counter_value(counter, {"outcome": "client_error", "tenant_id": "t-test"})
        assert after - before == 1.0

    def test_returns_outcome_string(self):
        counter = Counter(
            "test_record_outcome_returns_total",
            "Test counter",
            ["outcome", "tenant_id"],
        )
        result = record_outcome(counter, 202, tenant_id="t-test")
        assert result == "success"


# ---------------------------------------------------------------------------
# /metrics endpoint
# ---------------------------------------------------------------------------

class TestMetricsEndpoint:

    def test_metrics_response_content_type(self):
        """metrics_response() returns text/plain with a prometheus version string."""
        from prometheus_client import CONTENT_TYPE_LATEST
        resp = metrics_response()
        assert resp.media_type == CONTENT_TYPE_LATEST

    def test_metrics_response_contains_upload_type_lines(self):
        """Response body contains TYPE declarations for upload metrics."""
        resp = metrics_response()
        body = resp.body.decode()
        assert "# TYPE upload_total counter" in body
        assert "# TYPE upload_duration_seconds histogram" in body
        assert "# TYPE hmac_verify_duration_seconds histogram" in body

    def test_register_metrics_route_mounts_on_app(self):
        """register_metrics_route adds GET /metrics to the given FastAPI app."""
        app = FastAPI()
        register_metrics_route(app)
        client = TestClient(app)
        resp = client.get("/metrics")
        assert resp.status_code == 200
        assert "text/plain" in resp.headers["content-type"]

    def test_metrics_endpoint_contains_counter_and_histogram(self):
        """GET /metrics body includes at least one counter and one histogram for the sub-app domain."""
        app = FastAPI()
        register_metrics_route(app)
        client = TestClient(app)
        resp = client.get("/metrics")
        body = resp.text
        assert "# TYPE upload_total counter" in body
        assert "# TYPE upload_duration_seconds histogram" in body

    def test_tags_app_metrics_contains_tag_metrics(self):
        """tags sub-app /metrics includes tag_op_total and tag_endpoint_duration_seconds."""
        app = FastAPI()
        register_metrics_route(app)
        client = TestClient(app)
        resp = client.get("/metrics")
        body = resp.text
        assert "# TYPE tag_op_total counter" in body
        assert "# TYPE tag_endpoint_duration_seconds histogram" in body

    def test_domain_terms_app_metrics_contains_domain_term_metrics(self):
        """domain-terms sub-app /metrics includes domain_term_op_total and its histogram."""
        app = FastAPI()
        register_metrics_route(app)
        client = TestClient(app)
        resp = client.get("/metrics")
        body = resp.text
        assert "# TYPE domain_term_op_total counter" in body
        assert "# TYPE domain_term_endpoint_duration_seconds histogram" in body


# ---------------------------------------------------------------------------
# Upload instrumentation
# ---------------------------------------------------------------------------

_VALID_HMAC_CTX = {"tenant_id": "tenant-uuid-001", "node_id": "node-1", "user_id": "user-1", "role": "admin"}


def _make_upload_app_with_mock_hmac(hmac_ctx=_VALID_HMAC_CTX):
    """Return upload_app TestClient with HMAC middleware bypassed."""
    from api.upload import upload_app
    client = TestClient(upload_app, raise_server_exceptions=False)
    return client


class TestUploadInstrumentation:

    @patch("api.upload.put_object")
    @patch("api.upload.get_engine")
    @patch("api.upload.validate_extension")
    @patch("api.upload.validate_magic_bytes")
    @patch("api.upload.check_password_protected")
    @patch("api.upload.sanitize_filename", return_value="doc.pdf")
    @patch("api.upload.ensure_bucket")
    @patch("api.upload.validate_s3_path")
    @patch("api.middleware.verify_hmac_headers", return_value=_VALID_HMAC_CTX)
    def test_successful_upload_increments_success_counter(
        self, mock_hmac, mock_val_s3, mock_ensure, mock_san,
        mock_pwd, mock_magic, mock_ext, mock_engine, mock_put
    ):
        """Successful upload increments upload_total{outcome=success}."""

        mock_conn = MagicMock()
        mock_txn = MagicMock()
        mock_conn.begin.return_value = mock_txn
        mock_engine_instance = MagicMock()
        mock_engine_instance.connect.return_value = mock_conn
        mock_engine.return_value = mock_engine_instance

        tenant_id = _VALID_HMAC_CTX["tenant_id"]
        before = _get_counter_value(upload_total, {"outcome": "success", "tenant_id": tenant_id})

        from api.upload import upload_app
        client = TestClient(upload_app, raise_server_exceptions=False)
        import io
        resp = client.post(
            "/upload",
            files={"file": ("doc.pdf", io.BytesIO(b"%PDF-1.4 content"), "application/pdf")},
        )

        assert resp.status_code in (200, 202)
        after = _get_counter_value(upload_total, {"outcome": "success", "tenant_id": tenant_id})
        assert after - before == 1.0

    @patch("api.middleware.verify_hmac_headers", return_value=_VALID_HMAC_CTX)
    @patch("api.upload.validate_extension", side_effect=ValueError("bad extension"))
    def test_validation_failure_increments_client_error_counter(self, mock_ext, mock_hmac):
        """4xx upload response increments upload_total{outcome=client_error}."""
        tenant_id = _VALID_HMAC_CTX["tenant_id"]
        before = _get_counter_value(upload_total, {"outcome": "client_error", "tenant_id": tenant_id})

        from api.upload import upload_app
        client = TestClient(upload_app, raise_server_exceptions=False)
        import io
        resp = client.post(
            "/upload",
            files={"file": ("bad.exe", io.BytesIO(b"data"), "application/octet-stream")},
        )

        assert resp.status_code == 400
        after = _get_counter_value(upload_total, {"outcome": "client_error", "tenant_id": tenant_id})
        assert after - before == 1.0

    def test_hmac_failure_does_not_increment_upload_total(self):
        """HMAC 401 must NOT increment upload_total (route handler never entered)."""
        from textiq_hmac import VerificationError

        tenant_id = "hmac-fail-tenant"
        before_success = _get_counter_value(upload_total, {"outcome": "success", "tenant_id": tenant_id})
        before_client = _get_counter_value(upload_total, {"outcome": "client_error", "tenant_id": tenant_id})
        before_server = _get_counter_value(upload_total, {"outcome": "server_error", "tenant_id": tenant_id})

        with patch("api.middleware.verify_hmac_headers", side_effect=VerificationError("bad sig")):
            from api.upload import upload_app
            client = TestClient(upload_app, raise_server_exceptions=False)
            import io
            resp = client.post(
                "/upload",
                files={"file": ("doc.pdf", io.BytesIO(b"%PDF-1.4"), "application/pdf")},
            )

        assert resp.status_code == 401

        after_success = _get_counter_value(upload_total, {"outcome": "success", "tenant_id": tenant_id})
        after_client = _get_counter_value(upload_total, {"outcome": "client_error", "tenant_id": tenant_id})
        after_server = _get_counter_value(upload_total, {"outcome": "server_error", "tenant_id": tenant_id})

        assert after_success == before_success
        assert after_client == before_client
        assert after_server == before_server

    def test_hmac_failure_records_hmac_duration(self):
        """HMAC 401 must record hmac_verify_duration_seconds{result=fail}."""
        from textiq_hmac import VerificationError

        before = _get_histogram_count(hmac_verify_duration_seconds, {"result": "fail"})

        with patch("api.middleware.verify_hmac_headers", side_effect=VerificationError("bad sig")):
            from api.upload import upload_app
            client = TestClient(upload_app, raise_server_exceptions=False)
            import io
            client.post(
                "/upload",
                files={"file": ("doc.pdf", io.BytesIO(b"data"), "application/pdf")},
            )

        after = _get_histogram_count(hmac_verify_duration_seconds, {"result": "fail"})
        assert after - before >= 1.0


# ---------------------------------------------------------------------------
# duration histogram is recorded on upload
# ---------------------------------------------------------------------------

class TestUploadDurationHistogram:

    @patch("api.upload.put_object")
    @patch("api.upload.get_engine")
    @patch("api.upload.validate_extension")
    @patch("api.upload.validate_magic_bytes")
    @patch("api.upload.check_password_protected")
    @patch("api.upload.sanitize_filename", return_value="file.pdf")
    @patch("api.upload.ensure_bucket")
    @patch("api.upload.validate_s3_path")
    @patch("api.middleware.verify_hmac_headers", return_value=_VALID_HMAC_CTX)
    def test_upload_duration_observed_on_success(
        self, mock_hmac, mock_val_s3, mock_ensure, mock_san,
        mock_pwd, mock_magic, mock_ext, mock_engine, mock_put
    ):
        """upload_duration_seconds{outcome=success} _count increments after a successful upload."""

        mock_conn = MagicMock()
        mock_txn = MagicMock()
        mock_conn.begin.return_value = mock_txn
        mock_engine_instance = MagicMock()
        mock_engine_instance.connect.return_value = mock_conn
        mock_engine.return_value = mock_engine_instance

        before = _get_histogram_count(upload_duration_seconds, {"outcome": "success"})

        from api.upload import upload_app
        import io
        client = TestClient(upload_app, raise_server_exceptions=False)
        client.post(
            "/upload",
            files={"file": ("file.pdf", io.BytesIO(b"%PDF-1.4"), "application/pdf")},
        )

        after = _get_histogram_count(upload_duration_seconds, {"outcome": "success"})
        assert after - before == 1.0
