"""
Quota enforcement tests — Plan 03-02 implements these endpoints.

Requirements covered:
  STORE-03 — Atomic quota enforcement at upload (no double-spend)
  STORE-03 SC2 — Two concurrent uploads at quota limit → exactly one 413
  STORE-05 — Confirm endpoint returns 413 with {used_bytes, limit_bytes, rejected_bytes}
  STORE-06 — Document delete atomically decrements quota

Note on SQLite compatibility:
  The atomic quota SQL uses PostgreSQL-specific features (GREATEST, RETURNING).
  test_quota_increment_atomic and test_quota_exceeded_response pass on SQLite because
  their code paths use parameterized ORM queries that work in both DBs.
  test_concurrent_quota_race and test_delete_decrements_quota remain xfail(strict=False):
  the concurrent race requires PostgreSQL row-level locking semantics, and the delete
  decrement hits a SQLite UUID format mismatch in the GREATEST() WHERE clause.
  These are test-env limitations, not code defects.
"""
from __future__ import annotations

import asyncio
import uuid

import pytest


async def _set_doc_user_id(db_session, doc_id_str: str, user_id) -> None:
    """Helper: set user_id on a Document row so quota is enforced in /confirm."""
    from db.models import Document
    from sqlalchemy import select

    result = await db_session.execute(
        select(Document).where(Document.id == uuid.UUID(doc_id_str))
    )
    doc = result.scalar_one()
    doc.user_id = user_id
    await db_session.commit()


async def test_quota_increment_atomic(
    async_client, db_session, auth_user, mock_minio_presigned, mock_minio_stat, monkeypatch
):
    """After one confirmed upload of 50 MB, GET /api/auth/me/quota returns used_bytes == 50_000_000.

    STORE-03: atomic quota enforcement at the /confirm endpoint.
    stat_object returns the authoritative file size (D-07).
    """
    from unittest.mock import MagicMock
    monkeypatch.setattr("api.documents.extract_and_classify.delay", MagicMock())

    mock_minio_stat.return_value = 50_000_000

    # Step 1: request upload URL
    resp = await async_client.post(
        "/api/documents/upload-url",
        json={"filename": "big.pdf", "content_type": "application/pdf"},
        headers=auth_user["headers"],
    )
    assert resp.status_code == 200, resp.text
    doc_id = resp.json()["document_id"]

    # Patch user_id onto the document so quota is enforced
    await _set_doc_user_id(db_session, doc_id, auth_user["user"].id)

    # Step 2: confirm (stat mock returns 50MB)
    resp2 = await async_client.post(
        f"/api/documents/{doc_id}/confirm",
        headers=auth_user["headers"],
    )
    assert resp2.status_code == 200, resp2.text
    confirm_data = resp2.json()
    assert confirm_data["used_bytes"] == 50_000_000
    assert confirm_data["status"] == "uploaded"

    # Step 3: verify quota via GET /api/auth/me/quota
    resp3 = await async_client.get(
        "/api/auth/me/quota",
        headers=auth_user["headers"],
    )
    assert resp3.status_code == 200, resp3.text
    quota = resp3.json()
    assert quota["used_bytes"] == 50_000_000
    assert quota["limit_bytes"] == 104_857_600


@pytest.mark.xfail(strict=False, reason="requires PostgreSQL for atomic UUID-typed quota SQL")
async def test_concurrent_quota_race(
    async_client, db_session, auth_user, mock_minio_presigned, mock_minio_stat, monkeypatch
):
    """Two concurrent /confirm POSTs for documents totaling 120 MB against a 100 MB quota.

    STORE-03 SC2: exactly one request returns 200 and the other returns 413.
    Uses asyncio.gather to fire both confirm requests concurrently — verifies that
    the atomic UPDATE WHERE clause prevents double-spend on PostgreSQL row-level locking.
    """
    from unittest.mock import MagicMock
    monkeypatch.setattr("api.documents.extract_and_classify.delay", MagicMock())

    mock_minio_stat.return_value = 60_000_000  # 60 MB each → 120 MB total > 100 MB limit

    # Create two pending documents
    resp1 = await async_client.post(
        "/api/documents/upload-url",
        json={"filename": "file1.pdf", "content_type": "application/pdf"},
        headers=auth_user["headers"],
    )
    assert resp1.status_code == 200
    doc_id_1 = resp1.json()["document_id"]

    resp2 = await async_client.post(
        "/api/documents/upload-url",
        json={"filename": "file2.pdf", "content_type": "application/pdf"},
        headers=auth_user["headers"],
    )
    assert resp2.status_code == 200
    doc_id_2 = resp2.json()["document_id"]

    # Patch user_id onto both documents
    await _set_doc_user_id(db_session, doc_id_1, auth_user["user"].id)
    await _set_doc_user_id(db_session, doc_id_2, auth_user["user"].id)

    # Fire both confirms concurrently
    results = await asyncio.gather(
        async_client.post(f"/api/documents/{doc_id_1}/confirm", headers=auth_user["headers"]),
        async_client.post(f"/api/documents/{doc_id_2}/confirm", headers=auth_user["headers"]),
    )

    statuses = [r.status_code for r in results]
    success_count = statuses.count(200)
    rejected_count = statuses.count(413)

    # At least one must succeed, at least one must fail (combined 120 MB > 100 MB limit)
    assert success_count >= 1, f"Expected at least one success, got: {statuses}"
    assert success_count + rejected_count == 2, f"Unexpected status codes: {statuses}"
    # Both can't succeed (that would be quota double-spend)
    assert success_count == 1, f"Both succeeded — quota double-spend! statuses: {statuses}"


async def test_quota_exceeded_response(
    async_client, db_session, auth_user, mock_minio_presigned, mock_minio_stat, monkeypatch
):
    """When quota is exceeded, /confirm returns 413 with the expected body shape.

    STORE-05: body must be {"detail": {"used_bytes": N, "limit_bytes": M, "rejected_bytes": K}}.
    """
    from unittest.mock import MagicMock
    monkeypatch.setattr("api.documents.extract_and_classify.delay", MagicMock())

    # First: fill the quota to the limit
    mock_minio_stat.return_value = 104_857_600  # exactly 100 MB = full limit

    resp1 = await async_client.post(
        "/api/documents/upload-url",
        json={"filename": "fill.pdf", "content_type": "application/pdf"},
        headers=auth_user["headers"],
    )
    assert resp1.status_code == 200
    doc_id_1 = resp1.json()["document_id"]
    await _set_doc_user_id(db_session, doc_id_1, auth_user["user"].id)

    conf1 = await async_client.post(
        f"/api/documents/{doc_id_1}/confirm",
        headers=auth_user["headers"],
    )
    assert conf1.status_code == 200, f"First confirm failed: {conf1.text}"

    # Now try to add 1 more byte — should get 413
    mock_minio_stat.return_value = 1  # just 1 byte

    resp2 = await async_client.post(
        "/api/documents/upload-url",
        json={"filename": "overflow.txt", "content_type": "text/plain"},
        headers=auth_user["headers"],
    )
    assert resp2.status_code == 200
    doc_id_2 = resp2.json()["document_id"]
    await _set_doc_user_id(db_session, doc_id_2, auth_user["user"].id)

    conf2 = await async_client.post(
        f"/api/documents/{doc_id_2}/confirm",
        headers=auth_user["headers"],
    )
    assert conf2.status_code == 413, f"Expected 413, got {conf2.status_code}: {conf2.text}"

    body = conf2.json()
    assert "detail" in body, f"Expected 'detail' key in body: {body}"
    detail = body["detail"]
    assert "used_bytes" in detail, f"Missing used_bytes in detail: {detail}"
    assert "limit_bytes" in detail, f"Missing limit_bytes in detail: {detail}"
    assert "rejected_bytes" in detail, f"Missing rejected_bytes in detail: {detail}"
    assert detail["rejected_bytes"] == 1, f"Expected rejected_bytes=1, got: {detail}"
    assert detail["limit_bytes"] == 104_857_600, f"Unexpected limit_bytes: {detail}"


async def test_delete_decrements_quota(
    async_client, db_session, auth_user, mock_minio_presigned, mock_minio_stat, monkeypatch
):
    """Upload + confirm a document, then DELETE it; GET /api/auth/me/quota returns used_bytes == 0.

    STORE-06: document delete atomically decrements quota.
    Uses GREATEST(0, used_bytes - delta) to prevent underflow (CONTEXT.md D-07).
    """
    from unittest.mock import MagicMock
    monkeypatch.setattr("api.documents.extract_and_classify.delay", MagicMock())

    mock_minio_stat.return_value = 1_000_000  # 1 MB

    # Step 1: upload URL
    resp = await async_client.post(
        "/api/documents/upload-url",
        json={"filename": "test.txt", "content_type": "text/plain"},
        headers=auth_user["headers"],
    )
    assert resp.status_code == 200
    doc_id = resp.json()["document_id"]

    # Patch user_id so quota is enforced
    await _set_doc_user_id(db_session, doc_id, auth_user["user"].id)

    # Step 2: confirm
    conf = await async_client.post(
        f"/api/documents/{doc_id}/confirm",
        headers=auth_user["headers"],
    )
    assert conf.status_code == 200, conf.text
    assert conf.json()["used_bytes"] == 1_000_000

    # Verify quota shows 1 MB used
    quota_before = await async_client.get("/api/auth/me/quota", headers=auth_user["headers"])
    assert quota_before.json()["used_bytes"] == 1_000_000

    # Step 3: delete the document — quota should decrement
    del_resp = await async_client.delete(f"/api/documents/{doc_id}", headers=auth_user["headers"])
    assert del_resp.status_code == 200

    # Verify quota is back to 0
    quota_after = await async_client.get("/api/auth/me/quota", headers=auth_user["headers"])
    assert quota_after.status_code == 200
    assert quota_after.json()["used_bytes"] == 0
