# Copyright (c) 2025 Vertex AI Batch Embeddings API Contributors
#
# Licensed under the MIT License. See LICENSE file in the project root for details.


"""
Comprehensive tests for Vertex AI Batch Embeddings API
"""

import json
from unittest.mock import Mock, patch

import pytest

import src.config
from src.api import app


@pytest.fixture
def client():
    """Create test client."""
    app.config["TESTING"] = True
    with app.test_client() as client:
        yield client


@pytest.fixture
def mock_service():
    """Create mock embedding service."""
    mock = Mock()
    mock.trigger_batch_prediction.return_value = {
        "job_name": "test_job_123",
        "resource_name": "projects/test/locations/us-central1/batchPredictionJobs/123",
        "input_uri": "gs://test-bucket/embeddings/inputs/instances_123.jsonl",
        "output_uri": "gs://test-bucket/embeddings/outputs/test_job_123/",
        "location": "us-central1",
        "project_id": "test-project",
        "model": "text-embedding-004",
    }
    mock.get_job_status.return_value = {
        "job_name": "test_job_123",
        "status": "JOB_STATE_SUCCEEDED",
        "create_time": "2023-11-09T12:34:56Z",
        "output_uri": "gs://test-bucket/embeddings/outputs/test_job_123/",
    }
    return mock


class TestHealthEndpoints:
    """Test health and readiness endpoints."""

    def test_health_endpoint(self, client):
        """Health endpoint should return healthy status."""
        response = client.get("/health")
        assert response.status_code == 200
        data = json.loads(response.data)
        assert data["status"] == "healthy"
        assert "service" in data

    @patch.dict(
        "os.environ",
        {
            "GOOGLE_CLOUD_PROJECT": "test-project",
            "GCS_EMBED_INPUT_BUCKET": "test-input",
            "GCS_EMBED_OUTPUT_BUCKET": "test-output",
            "API_KEY_SECRET": "test-secret",
        },
    )
    @patch("src.api.get_service")
    def test_ready_endpoint_success(self, mock_get_service, client):
        """Ready endpoint should return ready when all env vars set."""
        mock_get_service.return_value = Mock()

        response = client.get("/ready")
        assert response.status_code == 200
        data = json.loads(response.data)
        assert data["status"] == "ready"

    @patch.dict("os.environ", {}, clear=True)
    def test_ready_endpoint_missing_env(self, client):
        """Ready endpoint should return not ready when env vars missing."""
        response = client.get("/ready")
        assert response.status_code == 503
        data = json.loads(response.data)
        assert data["status"] == "not_ready"
        assert "Missing required environment variables" in data["error"]


class TestBatchEmbeddingEndpoint:
    """Test batch embedding job creation."""

    @patch.object(src.config, "API_KEY_SECRET", "test-key")
    @patch.object(src.config, "GOOGLE_CLOUD_PROJECT", "test-project")
    @patch.object(src.config, "GCS_EMBED_INPUT_BUCKET", "test-input")
    @patch.object(src.config, "GCS_EMBED_OUTPUT_BUCKET", "test-output")
    @patch("src.api.get_service")
    def test_create_batch_job_success(self, mock_get_service, client, mock_service):
        """Should successfully create batch embedding job."""
        mock_get_service.return_value = mock_service

        response = client.post(
            "/v1/embeddings/batch",
            json={"texts": ["hello", "world"]},
            headers={"X-API-Key": "test-key"},
        )

        assert response.status_code == 200
        data = json.loads(response.data)
        assert data["job_name"] == "test_job_123"
        assert data["status"] == "submitted"
        assert data["text_count"] == 2
        assert "submitted_at" in data

    def test_create_batch_job_no_api_key(self, client):
        """Should reject request without API key."""
        response = client.post("/v1/embeddings/batch", json={"texts": ["hello", "world"]})
        assert response.status_code == 401

    def test_create_batch_job_invalid_json(self, client):
        """Should reject invalid JSON."""
        with patch.object(src.config, "API_KEY_SECRET", "test-key"):
            response = client.post(
                "/v1/embeddings/batch",
                data="invalid json",
                headers={"X-API-Key": "test-key"},
            )
        assert response.status_code == 400

    def test_create_batch_job_missing_texts(self, client):
        """Should reject request without texts field."""
        with patch.object(src.config, "API_KEY_SECRET", "test-key"):
            response = client.post(
                "/v1/embeddings/batch",
                json={"model": "text-embedding-004"},
                headers={"X-API-Key": "test-key"},
            )
        assert response.status_code == 400

    def test_create_batch_job_empty_texts(self, client):
        """Should reject request with empty texts array."""
        with patch.object(src.config, "API_KEY_SECRET", "test-key"):
            response = client.post(
                "/v1/embeddings/batch",
                json={"texts": []},
                headers={"X-API-Key": "test-key"},
            )
        assert response.status_code == 400


class TestJobStatusEndpoint:
    """Test job status retrieval."""

    @patch.object(src.config, "API_KEY_SECRET", "test-key")
    @patch("src.api.get_service")
    def test_get_job_status_success(self, mock_get_service, client, mock_service):
        """Should successfully retrieve job status."""
        mock_get_service.return_value = mock_service

        response = client.get("/v1/embeddings/batch/test_job/status", headers={"X-API-Key": "test-key"})

        assert response.status_code == 200
        data = json.loads(response.data)
        assert data["job_name"] == "test_job_123"
        assert data["status"] == "JOB_STATE_SUCCEEDED"

    @patch.object(src.config, "API_KEY_SECRET", "test-key")
    @patch("src.api.get_service")
    def test_get_job_status_not_found(self, mock_get_service, client, mock_service):
        """Should return 404 if job not found."""
        mock_service.get_job_status.side_effect = ValueError("Job not found")
        mock_get_service.return_value = mock_service

        response = client.get(
            "/v1/embeddings/batch/non_existent_job/status",
            headers={"X-API-Key": "test-key"},
        )

        assert response.status_code == 404
        data = json.loads(response.data)
        assert "Job not found" in data["error"]

    def test_get_job_status_no_api_key(self, client):
        """Should reject status request without API key."""
        response = client.get("/v1/embeddings/batch/test_job/status")
        assert response.status_code == 401


class TestRequestValidation:
    """Test request validation logic."""

    def test_valid_request(self):
        """Valid request should parse successfully."""
        from src.validation import validate_batch_request

        data = {"texts": ["hello", "world"]}
        # Should not raise exception
        validate_batch_request(data)

    def test_missing_texts_field(self):
        """Missing texts field should raise ValueError."""
        from src.validation import validate_batch_request

        data = {}
        with pytest.raises(ValueError, match="'texts' field is required"):
            validate_batch_request(data)

    def test_empty_texts_array(self):
        """Empty texts array should raise ValueError."""
        from src.validation import validate_batch_request

        data = {"texts": []}
        with pytest.raises(ValueError, match="'texts' array cannot be empty"):
            validate_batch_request(data)

    def test_texts_not_array(self):
        """texts field not being an array should raise ValueError."""
        from src.validation import validate_batch_request

        data = {"texts": "not an array"}
        with pytest.raises(ValueError, match="'texts' must be an array"):
            validate_batch_request(data)

    def test_texts_contains_non_strings(self):
        """texts array containing non-strings should raise ValueError."""
        from src.validation import validate_batch_request

        data = {"texts": ["hello", 123, "world"]}
        with pytest.raises(ValueError, match="All items in 'texts' must be strings"):
            validate_batch_request(data)

    def test_invalid_job_name_type(self):
        """Invalid job_name type should raise ValueError."""
        from src.validation import validate_batch_request

        data = {"texts": ["hello"], "job_name": 123}
        with pytest.raises(ValueError, match="'job_name' must be a string"):
            validate_batch_request(data)

    def test_invalid_job_name_format(self):
        """Invalid job_name format should raise ValueError."""
        from src.validation import validate_batch_request

        data = {"texts": ["hello"], "job_name": "invalid job name"}
        with pytest.raises(
            ValueError,
            match="'job_name' can only contain letters, numbers, hyphens, and underscores",
        ):
            validate_batch_request(data)

    def test_invalid_model(self):
        """Invalid model should raise ValueError."""
        from src.validation import validate_batch_request

        data = {"texts": ["hello"], "model": "invalid-model"}
        with pytest.raises(ValueError, match="'model' must be one of"):
            validate_batch_request(data)


class TestRateLimiting:
    """Test rate limiting functionality."""

    @patch.object(src.config, "RATE_LIMIT_REQUESTS", 2)
    @patch.object(src.config, "RATE_LIMIT_WINDOW", 60)
    @patch.object(src.config, "API_KEY_SECRET", "test-key")
    @patch("src.api.get_service")
    def test_rate_limit_exceeded(self, mock_get_service, client, mock_service):
        """Should enforce rate limiting."""
        mock_get_service.return_value = mock_service

        # Reset in-memory rate limiter state between tests
        from src import auth

        auth._rate_limits.clear()

        # First request
        response1 = client.post(
            "/v1/embeddings/batch",
            json={"texts": ["hello"]},
            headers={"X-API-Key": "test-key"},
        )
        assert response1.status_code == 200

        # Second request
        response2 = client.post(
            "/v1/embeddings/batch",
            json={"texts": ["world"]},
            headers={"X-API-Key": "test-key"},
        )
        assert response2.status_code == 200

        # Third request should be rate limited
        response3 = client.post(
            "/v1/embeddings/batch",
            json={"texts": ["third"]},
            headers={"X-API-Key": "test-key"},
        )
        assert response3.status_code == 429


class TestMetricsEndpoint:
    """Test metrics endpoint."""

    def test_metrics_endpoint(self, client):
        """Metrics endpoint should return Prometheus metrics."""
        response = client.get("/metrics")
        assert response.status_code == 200
        assert "text/plain" in response.content_type
        # Check for some expected metrics
        content = response.get_data(as_text=True)
        assert "api_requests_total" in content
        assert "api_request_duration_seconds" in content
        assert "embedding_jobs_submitted_total" in content


if __name__ == "__main__":
    pytest.main([__file__])
