"""
Comprehensive test suite for SaaS telemetry event logging system.

Tests cover:
- Idempotency (duplicate batch handling)
- Concurrency (parallel requests with overlapping events)
- Org validation (security enforcement)
- Job ID validation (multi-step action requirements)
- Query endpoints (summary, failures, daily rollup)
"""
from __future__ import annotations

import asyncio
from datetime import datetime, timezone
from uuid import uuid4

import pytest
from httpx import AsyncClient

from fmatch.saas.telemetry.schemas import MULTI_STEP_ACTIONS

pytestmark = pytest.mark.asyncio


# --- Test Data Factories ---


def _make_event_payload(
    org_id: str = "test_org_123",
    action: str = "match_completed",
    status: str = "completed",
    idempotency_key: str | None = None,
    job_id: str | None = None,
    credits_delta: int | None = None,
    source_version: str | None = None,
    **kwargs,
) -> dict:
    """Factory for creating test event payloads."""
    payload = {
        "org_id": org_id,
        "user_email": "test@example.com",
        "sheet_id": "test_sheet_abc123",
        "tab_name": "Sheet1",
        "source": "sheets_addon",
        "action": action,
        "status": status,
        "idempotency_key": idempotency_key or str(uuid4()),
        "attrs": {},
    }
    if job_id is None and action in MULTI_STEP_ACTIONS:
        job_id = f"job_{uuid4()}"
    if job_id is not None:
        payload["job_id"] = job_id
    if credits_delta is not None:
        payload["credits_delta"] = credits_delta
    if source_version is not None:
        payload["source_version"] = source_version
    payload.update(kwargs)
    return payload


def _auth_headers(org_id: str = "test_org_123", user_id: str = "test-user") -> dict:
    """Helper to ensure dependency override receives matching identity."""
    return {
        "X-Test-Org": org_id,
        "X-Test-User": user_id,
        "X-Test-Email": f"{user_id}@example.com",
    }


# --- Idempotency Tests ---


async def test_ingest_batch_idempotency_same_batch_twice(async_client: AsyncClient):
    """Sending the same batch twice should result in zero duplicates on second send."""
    from fmatch.saas.main import app as _app
    from fmatch.saas.auth_core import get_current_identity as _gci

    assert _gci in _app.dependency_overrides, "get_current_identity override missing"
    org_id = "org_idempotency_test"
    events = [
        _make_event_payload(org_id=org_id, action="match_started", job_id="job_1"),
        _make_event_payload(org_id=org_id, action="match_completed", job_id="job_1"),
    ]
    payload = {"events": events}

    # First submission
    resp1 = await async_client.post(
        "/api/v1/telemetry/events:batch",
        json=payload,
        headers=_auth_headers(org_id),
    )
    assert resp1.status_code == 200
    data1 = resp1.json()
    assert data1["inserted"] == 2
    assert data1["duplicates"] == 0

    # Second submission (exact same batch)
    resp2 = await async_client.post(
        "/api/v1/telemetry/events:batch",
        json=payload,
        headers=_auth_headers(org_id),
    )
    assert resp2.status_code == 200
    data2 = resp2.json()
    assert data2["inserted"] == 0
    assert data2["duplicates"] == 2


async def test_ingest_batch_partial_overlap(async_client: AsyncClient):
    """Batch with some new and some duplicate events should handle correctly."""
    org_id = "org_partial_overlap"
    shared_key = str(uuid4())

    # First batch: 2 events
    batch1 = {
        "events": [
            _make_event_payload(org_id=org_id, idempotency_key=shared_key),
            _make_event_payload(org_id=org_id, idempotency_key=str(uuid4())),
        ]
    }
    resp1 = await async_client.post(
        "/api/v1/telemetry/events:batch",
        json=batch1,
        headers=_auth_headers(org_id),
    )
    assert resp1.status_code == 200
    assert resp1.json()["inserted"] == 2

    # Second batch: 1 duplicate (shared_key) + 2 new
    batch2 = {
        "events": [
            _make_event_payload(org_id=org_id, idempotency_key=shared_key),  # duplicate
            _make_event_payload(org_id=org_id, idempotency_key=str(uuid4())),  # new
            _make_event_payload(org_id=org_id, idempotency_key=str(uuid4())),  # new
        ]
    }
    resp2 = await async_client.post(
        "/api/v1/telemetry/events:batch",
        json=batch2,
        headers=_auth_headers(org_id),
    )
    assert resp2.status_code == 200
    data2 = resp2.json()
    assert data2["inserted"] == 2
    assert data2["duplicates"] == 1


async def test_ingest_empty_batch_rejected(async_client: AsyncClient):
    """Empty event batches should be rejected with validation error."""
    resp = await async_client.post(
        "/api/v1/telemetry/events:batch",
        json={"events": []},
        headers=_auth_headers(),
    )
    assert resp.status_code == 422  # Pydantic validation error


# --- Concurrency Tests ---


async def test_concurrent_batch_inserts_same_idempotency_keys(async_client: AsyncClient):
    """Parallel requests with identical idempotency keys should not create duplicates."""
    org_id = "org_concurrent_test"
    shared_keys = [str(uuid4()) for _ in range(3)]

    # Create two identical batches
    batch = {
        "events": [
            _make_event_payload(org_id=org_id, idempotency_key=key)
            for key in shared_keys
        ]
    }

    # Send both batches concurrently
    results = await asyncio.gather(
        async_client.post(
            "/api/v1/telemetry/events:batch",
            json=batch,
            headers=_auth_headers(org_id),
        ),
        async_client.post(
            "/api/v1/telemetry/events:batch",
            json=batch,
            headers=_auth_headers(org_id),
        ),
    )

    # Both should succeed
    assert all(r.status_code == 200 for r in results)

    # Total inserted across both requests should be exactly 3 (no duplicates)
    total_inserted = sum(r.json()["inserted"] for r in results)
    total_duplicates = sum(r.json()["duplicates"] for r in results)
    assert total_inserted == 3
    assert total_duplicates == 3


async def test_concurrent_batch_inserts_different_events(async_client: AsyncClient):
    """Parallel requests with different events should all succeed."""
    org_id = "org_concurrent_different"

    batch1 = {
        "events": [
            _make_event_payload(org_id=org_id, idempotency_key=str(uuid4()))
            for _ in range(3)
        ]
    }
    batch2 = {
        "events": [
            _make_event_payload(org_id=org_id, idempotency_key=str(uuid4()))
            for _ in range(3)
        ]
    }

    results = await asyncio.gather(
        async_client.post(
            "/api/v1/telemetry/events:batch",
            json=batch1,
            headers=_auth_headers(org_id),
        ),
        async_client.post(
            "/api/v1/telemetry/events:batch",
            json=batch2,
            headers=_auth_headers(org_id),
        ),
    )

    assert all(r.status_code == 200 for r in results)
    assert sum(r.json()["inserted"] for r in results) == 6
    assert sum(r.json()["duplicates"] for r in results) == 0


# --- Org Validation Tests ---


async def test_org_mismatch_rejected(async_client: AsyncClient):
    """Events with org_id != authenticated org should be rejected."""
    expected_org = "correct_org_123"
    wrong_org = "different_org_123"
    batch = {
        "events": [
            _make_event_payload(org_id=wrong_org),
        ]
    }

    resp = await async_client.post(
        "/api/v1/telemetry/events:batch",
        json=batch,
        headers=_auth_headers(expected_org),
    )
    # Should fail with 400 or 403 due to org mismatch
    # The actual endpoint returns 400 with "ORG_MISMATCH" error
    assert resp.status_code in (400, 403)
    if resp.status_code == 400:
        error = resp.json().get("detail", {})
        if isinstance(error, dict):
            assert error.get("error") == "ORG_MISMATCH"


# --- Job ID Validation Tests ---


async def test_multi_step_action_requires_job_id(async_client: AsyncClient):
    """Actions like match_started/completed should require job_id."""
    org_id = "org_job_id_validation"

    # Missing job_id for multi-step action
    batch = {
        "events": [
            {
                "org_id": org_id,
                "user_email": "test@example.com",
                "sheet_id": "test_sheet_abc123",
                "tab_name": "Sheet1",
                "source": "sheets_addon",
                "action": "match_started",
                "status": "started",
                "idempotency_key": str(uuid4()),
                "attrs": {},
            }
        ]
    }

    resp = await async_client.post(
        "/api/v1/telemetry/events:batch",
        json=batch,
        headers=_auth_headers(org_id),
    )
    assert resp.status_code == 422  # Pydantic validation error


async def test_multi_step_action_with_job_id_succeeds(async_client: AsyncClient):
    """Multi-step actions with job_id should succeed."""
    org_id = "org_job_id_valid"
    job_id = str(uuid4())

    batch = {
        "events": [
            _make_event_payload(
                org_id=org_id,
                action="match_started",
                status="started",
                job_id=job_id,
            ),
            _make_event_payload(
                org_id=org_id,
                action="match_completed",
                status="completed",
                job_id=job_id,
                duration_ms=5000,
                rows=100,
            ),
        ]
    }

    resp = await async_client.post(
        "/api/v1/telemetry/events:batch",
        json=batch,
        headers=_auth_headers(org_id),
    )
    assert resp.status_code == 200
    assert resp.json()["inserted"] == 2


async def test_enrichment_with_credits_tracking(async_client: AsyncClient):
    """Enrichment events should track credits and support balance metadata."""
    org_id = "org_credits_test"
    job_id = str(uuid4())

    batch = {
        "events": [
            _make_event_payload(
                org_id=org_id,
                action="enrichment_started",
                status="started",
                job_id=job_id,
            ),
            _make_event_payload(
                org_id=org_id,
                action="enrichment_completed",
                status="completed",
                job_id=job_id,
                credits_delta=-10,  # Negative = debit
                rows=5,
                attrs={
                    "credits_balance_before": 100,
                    "credits_balance_after": 90,
                },
            ),
        ]
    }

    resp = await async_client.post(
        "/api/v1/telemetry/events:batch",
        json=batch,
        headers=_auth_headers(org_id),
    )
    assert resp.status_code == 200
    assert resp.json()["inserted"] == 2


# --- Source Version Tests ---


async def test_source_version_tracking(async_client: AsyncClient):
    """Events should accept and persist source_version for forensics."""
    org_id = "org_version_test"

    batch = {
        "events": [
            _make_event_payload(
                org_id=org_id,
                action="detect_ran",
                status="success",
                source_version="v2.3.1",
            ),
        ]
    }

    resp = await async_client.post(
        "/api/v1/telemetry/events:batch",
        json=batch,
        headers=_auth_headers(org_id),
    )
    assert resp.status_code == 200
    assert resp.json()["inserted"] == 1


# --- Query Endpoint Tests ---


async def test_summary_endpoint_empty_org(async_client: AsyncClient):
    """Summary for org with no events should return zero counts."""
    org_id = "org_summary_empty"
    resp = await async_client.get(
        "/api/v1/telemetry/summary",
        headers=_auth_headers(org_id),
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["total_events"] == 0
    assert data["failures"] == 0
    assert data["credits_used"] == 0
    assert data["last_event_at"] is None
    assert data["top_actions"] == []


async def test_summary_endpoint_with_events(async_client: AsyncClient):
    """Summary should aggregate events correctly."""
    org_id = f"org_summary_{uuid4()}"
    job_id = str(uuid4())

    # Insert test events
    batch = {
        "events": [
            _make_event_payload(
                org_id=org_id,
                action="enrichment_completed",
                status="completed",
                job_id=job_id,
                credits_delta=-15,
            ),
            _make_event_payload(
                org_id=org_id,
                action="match_completed",
                status="completed",
                job_id=str(uuid4()),
            ),
            _make_event_payload(
                org_id=org_id,
                action="sf_push_failed",
                status="failed",
                job_id=str(uuid4()),
                attrs={"error_code": "INVALID_SESSION_ID"},
            ),
        ]
    }
    await async_client.post(
        "/api/v1/telemetry/events:batch",
        json=batch,
        headers=_auth_headers(org_id),
    )

    resp = await async_client.get(
        "/api/v1/telemetry/summary",
        headers=_auth_headers(org_id),
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["total_events"] == 3
    assert data["failures"] == 1
    assert data["credits_used"] == -15
    assert any(item["action"] == "enrichment_completed" for item in data["top_actions"])


async def test_failures_endpoint(async_client: AsyncClient):
    """Failures endpoint should return only failed events."""
    org_id = "org_failures_test"

    batch = {
        "events": [
            _make_event_payload(
                org_id=org_id,
                action="sf_push_failed",
                status="failed",
                job_id=str(uuid4()),
                attrs={"error_code": "INVALID_FIELD"},
            ),
            _make_event_payload(
                org_id=org_id,
                action="enrichment_failed",
                status="failed",
                job_id=str(uuid4()),
                attrs={"error_code": "RATE_LIMIT"},
            ),
            _make_event_payload(
                org_id=org_id,
                action="match_completed",
                status="completed",
                job_id=str(uuid4()),
            ),
        ]
    }
    await async_client.post(
        "/api/v1/telemetry/events:batch",
        json=batch,
        headers=_auth_headers(org_id),
    )

    resp = await async_client.get(
        "/api/v1/telemetry/failures",
        headers=_auth_headers(org_id),
    )

    assert resp.status_code == 200
    failures = resp.json()
    assert isinstance(failures, list)
    # All returned events should have status="failed"
    for failure in failures:
        assert failure["status"] == "failed"


async def test_failures_endpoint_filter_by_action(async_client: AsyncClient):
    """Failures endpoint should support action filtering."""
    org_id = "org_failures_filter"

    batch = {
        "events": [
            _make_event_payload(
                org_id=org_id,
                action="sf_push_failed",
                status="failed",
                job_id=str(uuid4()),
            ),
            _make_event_payload(
                org_id=org_id,
                action="enrichment_failed",
                status="failed",
                job_id=str(uuid4()),
            ),
        ]
    }
    await async_client.post(
        "/api/v1/telemetry/events:batch",
        json=batch,
        headers=_auth_headers(org_id),
    )

    resp = await async_client.get(
        "/api/v1/telemetry/failures?action=sf_push_failed",
        headers=_auth_headers(org_id),
    )

    assert resp.status_code == 200
    failures = resp.json()
    for failure in failures:
        assert failure["action"] == "sf_push_failed"


async def test_daily_credits_endpoint(async_client: AsyncClient):
    """Daily credits endpoint should return aggregated rollup."""
    resp = await async_client.get(
        "/api/v1/telemetry/credits/daily?days=7",
        headers=_auth_headers(),
    )
    assert resp.status_code == 200
    data = resp.json()
    assert "org_id" in data
    assert "points" in data
    assert isinstance(data["points"], list)


# --- Edge Cases ---


async def test_missing_required_fields_rejected(async_client: AsyncClient):
    """Events missing required fields should be rejected."""
    invalid_batch = {
        "events": [
            {
                "org_id": "test_org",
                # Missing user_email, sheet_id, action, status, idempotency_key
            }
        ]
    }

    resp = await async_client.post(
        "/api/v1/telemetry/events:batch",
        json=invalid_batch,
        headers=_auth_headers("test_org"),
    )
    assert resp.status_code == 422


async def test_invalid_email_rejected(async_client: AsyncClient):
    """Events with invalid email should be rejected."""
    batch = {
        "events": [
            _make_event_payload(user_email="not-an-email"),
        ]
    }

    resp = await async_client.post(
        "/api/v1/telemetry/events:batch",
        json=batch,
        headers=_auth_headers(),
    )
    assert resp.status_code == 422


async def test_timestamp_normalization(async_client: AsyncClient):
    """Timestamps should be normalized to UTC."""
    org_id = "org_timestamp_test"

    # Send event with explicit timestamp
    now_utc = datetime.now(timezone.utc)
    batch = {
        "events": [
            {
                **_make_event_payload(org_id=org_id),
                "ts": now_utc.isoformat(),
            }
        ]
    }

    resp = await async_client.post(
        "/api/v1/telemetry/events:batch",
        json=batch,
        headers=_auth_headers(org_id),
    )
    assert resp.status_code == 200
    assert resp.json()["inserted"] == 1
