"""Tests for presigned download/preview URL endpoints.

These tests exercise the Airflow plugin endpoints at:
  GET /{document_id}/download-url
  GET /{document_id}/preview-url

External dependencies (HMAC auth, DB, S3) are mocked because
the plugin runs inside the Airflow api-server process with its own infra.
"""

from unittest.mock import MagicMock, patch
from uuid import uuid4

import pytest
from fastapi.testclient import TestClient


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

TENANT_ID = str(uuid4())
NODE_ID = str(uuid4())
USER_ID = str(uuid4())

HMAC_CTX = {
    "tenant_id": TENANT_ID,
    "node_id": NODE_ID,
    "user_id": USER_ID,
    "role": "admin",
}


@pytest.fixture()
def client():
    """TestClient with HMAC verification bypassed."""
    with patch("api.auth.verify_hmac_headers", return_value=HMAC_CTX):
        from api.upload import upload_app

        yield TestClient(upload_app, raise_server_exceptions=False)


@pytest.fixture()
def client_no_hmac():
    """TestClient with HMAC verification raising VerificationError (no auth)."""
    from api.auth import VerificationError

    with patch(
        "api.middleware.verify_hmac_headers",
        side_effect=VerificationError("Missing signature"),
    ):
        from api.upload import upload_app

        yield TestClient(upload_app, raise_server_exceptions=False)


def _make_doc_row(doc_id, filename="report.xlsx", file_size_bytes=1024):
    """Build a fake DB row mapping matching the SELECT columns."""
    file_path = f"{TENANT_ID}/{NODE_ID}/{doc_id}/{filename}"
    row = MagicMock()
    row._mapping = {
        "id": str(doc_id),
        "filename": filename,
        "file_path": file_path,
        "file_size_bytes": file_size_bytes,
    }
    return row


def _mock_rls(mock_rls, row):
    """Wire up rls_connection mock to return a row from fetchone."""
    mock_conn = MagicMock()
    mock_conn.execute.return_value.fetchone.return_value = row
    mock_rls.return_value.__enter__ = MagicMock(return_value=mock_conn)
    mock_rls.return_value.__exit__ = MagicMock(return_value=False)
    return mock_conn


# ---------------------------------------------------------------------------
# Download URL — happy paths
# ---------------------------------------------------------------------------


class TestDownloadUrlHappyPaths:
    """GET /{id}/download-url happy-path scenarios."""

    @patch("api.upload._get_presign_s3_client")
    @patch("api.upload.get_s3_client")
    @patch("api.upload.get_upload_bucket", return_value="test-bucket")
    @patch("api.upload.rls_connection")
    def test_download_xlsx_returns_attachment_disposition(
        self, mock_rls, mock_bucket, mock_s3, mock_presign_s3, client
    ):
        doc_id = uuid4()
        row = _make_doc_row(doc_id, filename="data.xlsx", file_size_bytes=5000)
        _mock_rls(mock_rls, row)
        mock_s3.return_value.head_object.return_value = {}
        mock_presign_s3.return_value.generate_presigned_url.return_value = "https://s3/presigned"

        resp = client.get(f"/{doc_id}/download-url")

        assert resp.status_code == 200
        data = resp.json()
        assert data["url"] == "https://s3/presigned"
        assert data["filename"] == "data.xlsx"
        assert data["content_type"] == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        assert data["file_size_bytes"] == 5000
        assert data["expires_in"] == 3600
        assert data["inline"] is False

    @patch("api.upload._get_presign_s3_client")
    @patch("api.upload.get_s3_client")
    @patch("api.upload.get_upload_bucket", return_value="test-bucket")
    @patch("api.upload.rls_connection")
    def test_download_pdf_returns_attachment_not_inline(
        self, mock_rls, mock_bucket, mock_s3, mock_presign_s3, client
    ):
        """Download endpoint always uses attachment, even for PDFs."""
        doc_id = uuid4()
        row = _make_doc_row(doc_id, filename="report.pdf")
        _mock_rls(mock_rls, row)
        mock_s3.return_value.head_object.return_value = {}
        mock_presign_s3.return_value.generate_presigned_url.return_value = "https://s3/presigned"

        resp = client.get(f"/{doc_id}/download-url")

        assert resp.status_code == 200
        data = resp.json()
        assert data["inline"] is False
        assert data["content_type"] == "application/pdf"

    @patch("api.upload._get_presign_s3_client")
    @patch("api.upload.get_s3_client")
    @patch("api.upload.get_upload_bucket", return_value="test-bucket")
    @patch("api.upload.rls_connection")
    def test_download_passes_correct_params_to_presign(
        self, mock_rls, mock_bucket, mock_s3, mock_presign_s3, client
    ):
        doc_id = uuid4()
        filename = "test file.xlsx"
        row = _make_doc_row(doc_id, filename=filename)
        _mock_rls(mock_rls, row)
        mock_s3.return_value.head_object.return_value = {}
        mock_presign_s3.return_value.generate_presigned_url.return_value = "https://s3/presigned"

        client.get(f"/{doc_id}/download-url")

        call_kwargs = mock_presign_s3.return_value.generate_presigned_url.call_args
        params = call_kwargs[1]["Params"] if "Params" in call_kwargs[1] else call_kwargs[0][1]
        assert params["Bucket"] == "test-bucket"
        assert params["Key"] == row._mapping["file_path"]
        assert "attachment" in params["ResponseContentDisposition"]
        assert call_kwargs[1]["ExpiresIn"] == 3600


# ---------------------------------------------------------------------------
# Preview URL — happy paths
# ---------------------------------------------------------------------------


class TestPreviewUrlHappyPaths:
    """GET /{id}/preview-url happy-path scenarios."""

    @patch("api.upload._get_presign_s3_client")
    @patch("api.upload.get_s3_client")
    @patch("api.upload.get_upload_bucket", return_value="test-bucket")
    @patch("api.upload.rls_connection")
    def test_preview_pdf_returns_inline_true(
        self, mock_rls, mock_bucket, mock_s3, mock_presign_s3, client
    ):
        doc_id = uuid4()
        row = _make_doc_row(doc_id, filename="report.pdf")
        _mock_rls(mock_rls, row)
        mock_s3.return_value.head_object.return_value = {}
        mock_presign_s3.return_value.generate_presigned_url.return_value = "https://s3/presigned"

        resp = client.get(f"/{doc_id}/preview-url")

        assert resp.status_code == 200
        data = resp.json()
        assert data["inline"] is True
        assert data["content_type"] == "application/pdf"

    @patch("api.upload._get_presign_s3_client")
    @patch("api.upload.get_s3_client")
    @patch("api.upload.get_upload_bucket", return_value="test-bucket")
    @patch("api.upload.rls_connection")
    def test_preview_pdf_disposition_is_inline(
        self, mock_rls, mock_bucket, mock_s3, mock_presign_s3, client
    ):
        doc_id = uuid4()
        row = _make_doc_row(doc_id, filename="report.pdf")
        _mock_rls(mock_rls, row)
        mock_s3.return_value.head_object.return_value = {}
        mock_presign_s3.return_value.generate_presigned_url.return_value = "https://s3/presigned"

        client.get(f"/{doc_id}/preview-url")

        call_kwargs = mock_presign_s3.return_value.generate_presigned_url.call_args
        params = call_kwargs[1]["Params"] if "Params" in call_kwargs[1] else call_kwargs[0][1]
        assert "inline" in params["ResponseContentDisposition"]

    @patch("api.upload._get_presign_s3_client")
    @patch("api.upload.get_s3_client")
    @patch("api.upload.get_upload_bucket", return_value="test-bucket")
    @patch("api.upload.rls_connection")
    def test_preview_non_pdf_falls_back_to_attachment(
        self, mock_rls, mock_bucket, mock_s3, mock_presign_s3, client
    ):
        doc_id = uuid4()
        row = _make_doc_row(doc_id, filename="data.xlsx")
        _mock_rls(mock_rls, row)
        mock_s3.return_value.head_object.return_value = {}
        mock_presign_s3.return_value.generate_presigned_url.return_value = "https://s3/presigned"

        resp = client.get(f"/{doc_id}/preview-url")

        assert resp.status_code == 200
        data = resp.json()
        assert data["inline"] is False

        call_kwargs = mock_presign_s3.return_value.generate_presigned_url.call_args
        params = call_kwargs[1]["Params"] if "Params" in call_kwargs[1] else call_kwargs[0][1]
        assert "attachment" in params["ResponseContentDisposition"]


# ---------------------------------------------------------------------------
# 404 — document not found / cross-tenant / S3 missing
# ---------------------------------------------------------------------------


class TestNotFound:
    """Document not found, cross-tenant, or S3 object missing → 404."""

    @patch("api.upload.rls_connection")
    def test_nonexistent_doc_returns_404(self, mock_rls, client):
        doc_id = uuid4()
        _mock_rls(mock_rls, None)

        resp = client.get(f"/{doc_id}/download-url")

        assert resp.status_code == 404
        assert resp.json()["error"]["code"] == "NOT_FOUND"

    @patch("api.upload.rls_connection")
    def test_cross_tenant_returns_404(self, mock_rls, client):
        doc_id = uuid4()
        _mock_rls(mock_rls, None)  # RLS filters out the row

        resp = client.get(f"/{doc_id}/preview-url")

        assert resp.status_code == 404
        assert resp.json()["error"]["code"] == "NOT_FOUND"

    @patch("api.upload.get_s3_client")
    @patch("api.upload.get_upload_bucket", return_value="test-bucket")
    @patch("api.upload.rls_connection")
    def test_s3_object_missing_returns_404(self, mock_rls, mock_bucket, mock_s3, client):
        from botocore.exceptions import ClientError

        doc_id = uuid4()
        row = _make_doc_row(doc_id)
        _mock_rls(mock_rls, row)
        mock_s3.return_value.head_object.side_effect = ClientError(
            {"Error": {"Code": "404", "Message": "Not Found"}}, "HeadObject"
        )

        resp = client.get(f"/{doc_id}/download-url")

        assert resp.status_code == 404
        assert resp.json()["error"]["code"] == "FILE_NOT_FOUND"

    @patch("api.upload.get_s3_client")
    @patch("api.upload.get_upload_bucket", return_value="test-bucket")
    @patch("api.upload.rls_connection")
    def test_s3_nosuchkey_returns_404(self, mock_rls, mock_bucket, mock_s3, client):
        """SeaweedFS may return NoSuchKey instead of 404."""
        from botocore.exceptions import ClientError

        doc_id = uuid4()
        row = _make_doc_row(doc_id)
        _mock_rls(mock_rls, row)
        mock_s3.return_value.head_object.side_effect = ClientError(
            {"Error": {"Code": "NoSuchKey", "Message": "Not Found"}}, "HeadObject"
        )

        resp = client.get(f"/{doc_id}/download-url")

        assert resp.status_code == 404
        assert resp.json()["error"]["code"] == "FILE_NOT_FOUND"


# ---------------------------------------------------------------------------
# 422 — validation errors
# ---------------------------------------------------------------------------


class TestValidationErrors:
    """Invalid UUID → 422."""

    def test_download_invalid_uuid_returns_422(self, client):
        resp = client.get("/not-a-uuid/download-url")
        assert resp.status_code == 422
        assert resp.json()["error"]["code"] == "INVALID_ID"

    def test_preview_invalid_uuid_returns_422(self, client):
        resp = client.get("/not-a-uuid/preview-url")
        assert resp.status_code == 422
        assert resp.json()["error"]["code"] == "INVALID_ID"


# ---------------------------------------------------------------------------
# 401 — HMAC authentication
# ---------------------------------------------------------------------------


class TestHMACAuth:
    """Missing or invalid HMAC headers → 401."""

    def test_download_without_hmac_returns_401(self, client_no_hmac):
        doc_id = uuid4()
        resp = client_no_hmac.get(f"/{doc_id}/download-url")
        assert resp.status_code == 401

    def test_preview_without_hmac_returns_401(self, client_no_hmac):
        doc_id = uuid4()
        resp = client_no_hmac.get(f"/{doc_id}/preview-url")
        assert resp.status_code == 401


# ---------------------------------------------------------------------------
# Content type inference
# ---------------------------------------------------------------------------


class TestContentTypeInference:
    """Verify _infer_content_type returns correct MIME types."""

    @patch("api.upload._get_presign_s3_client")
    @patch("api.upload.get_s3_client")
    @patch("api.upload.get_upload_bucket", return_value="test-bucket")
    @patch("api.upload.rls_connection")
    def test_pdf_content_type(self, mock_rls, mock_bucket, mock_s3, mock_presign_s3, client):
        doc_id = uuid4()
        row = _make_doc_row(doc_id, filename="file.pdf")
        _mock_rls(mock_rls, row)
        mock_s3.return_value.head_object.return_value = {}
        mock_presign_s3.return_value.generate_presigned_url.return_value = "https://s3/presigned"

        resp = client.get(f"/{doc_id}/download-url")
        assert resp.json()["content_type"] == "application/pdf"

    @patch("api.upload._get_presign_s3_client")
    @patch("api.upload.get_s3_client")
    @patch("api.upload.get_upload_bucket", return_value="test-bucket")
    @patch("api.upload.rls_connection")
    def test_xlsx_content_type(self, mock_rls, mock_bucket, mock_s3, mock_presign_s3, client):
        doc_id = uuid4()
        row = _make_doc_row(doc_id, filename="file.xlsx")
        _mock_rls(mock_rls, row)
        mock_s3.return_value.head_object.return_value = {}
        mock_presign_s3.return_value.generate_presigned_url.return_value = "https://s3/presigned"

        resp = client.get(f"/{doc_id}/download-url")
        assert resp.json()["content_type"] == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"

    @patch("api.upload._get_presign_s3_client")
    @patch("api.upload.get_s3_client")
    @patch("api.upload.get_upload_bucket", return_value="test-bucket")
    @patch("api.upload.rls_connection")
    def test_unknown_extension_defaults_to_octet_stream(
        self, mock_rls, mock_bucket, mock_s3, mock_presign_s3, client
    ):
        doc_id = uuid4()
        row = _make_doc_row(doc_id, filename="file.xyz123")
        _mock_rls(mock_rls, row)
        mock_s3.return_value.head_object.return_value = {}
        mock_presign_s3.return_value.generate_presigned_url.return_value = "https://s3/presigned"

        resp = client.get(f"/{doc_id}/download-url")
        assert resp.json()["content_type"] == "application/octet-stream"
