"""Tests for decks API endpoints.

Tests the deck management endpoints with mocked services.
The backend is stateless - DeckConfig comes from the request.
"""

from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, patch
from uuid import uuid4

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

from app.api import deps as api_deps
from app.api.decks import router as decks_router
from app.db.models import User
from app.repositories.protocols import DeckEntry
from app.services.card_service import CardService
from app.services.collection_service import CollectionService
from app.services.deck_service import (
    DeckLimitExceededError,
    DeckNotFoundError,
    DeckService,
)
from app.services.deck_validator import ValidationResult
from app.services.jwt_service import create_access_token

# =============================================================================
# Fixtures
# =============================================================================


@pytest.fixture
def test_user():
    """Create a test user object."""
    user = User(
        email="test@example.com",
        display_name="Test User",
        avatar_url="https://example.com/avatar.jpg",
        oauth_provider="google",
        oauth_id="google-123",
        is_premium=False,
        premium_until=None,
    )
    user.id = uuid4()
    user.created_at = datetime.now(UTC)
    user.updated_at = datetime.now(UTC)
    user.last_login = None
    user.linked_accounts = []
    return user


@pytest.fixture
def premium_user(test_user):
    """Create a premium test user."""
    from datetime import timedelta

    test_user.is_premium = True
    test_user.premium_until = datetime.now(UTC) + timedelta(days=30)
    return test_user


@pytest.fixture
def access_token(test_user):
    """Create a valid access token for the test user."""
    return create_access_token(test_user.id)


@pytest.fixture
def auth_headers(access_token):
    """Create Authorization headers with Bearer token."""
    return {"Authorization": f"Bearer {access_token}"}


@pytest.fixture
def mock_deck_service():
    """Create a mock DeckService."""
    return MagicMock(spec=DeckService)


@pytest.fixture
def mock_collection_service():
    """Create a mock CollectionService."""
    return MagicMock(spec=CollectionService)


@pytest.fixture
def mock_card_service():
    """Create a mock CardService."""
    service = MagicMock(spec=CardService)
    service.get_card.return_value = MagicMock()  # Card exists
    return service


@pytest.fixture
def app(test_user, mock_deck_service, mock_collection_service, mock_card_service):
    """Create a test FastAPI app with mocked dependencies."""
    test_app = FastAPI()
    test_app.include_router(decks_router, prefix="/api")

    # Override dependencies
    async def override_get_current_user():
        return test_user

    def override_get_deck_service():
        return mock_deck_service

    def override_get_collection_service():
        return mock_collection_service

    test_app.dependency_overrides[api_deps.get_current_user] = override_get_current_user
    test_app.dependency_overrides[api_deps.get_deck_service] = override_get_deck_service
    test_app.dependency_overrides[api_deps.get_collection_service] = override_get_collection_service

    yield test_app
    test_app.dependency_overrides.clear()


@pytest.fixture
def unauthenticated_app():
    """Create a test FastAPI app without auth override (for 401 tests)."""
    test_app = FastAPI()
    test_app.include_router(decks_router, prefix="/api")
    yield test_app


@pytest.fixture
def client(app):
    """Create a test client for the app."""
    return TestClient(app)


@pytest.fixture
def unauthenticated_client(unauthenticated_app):
    """Create a test client without auth for 401 tests."""
    return TestClient(unauthenticated_app)


def make_deck_entry(
    name: str = "Test Deck",
    is_valid: bool = True,
    is_starter: bool = False,
) -> DeckEntry:
    """Create a DeckEntry for testing."""
    return DeckEntry(
        id=uuid4(),
        user_id=uuid4(),
        name=name,
        cards={"a1-001-bulbasaur": 4, "a1-033-charmander": 4},
        energy_cards={"grass": 10, "fire": 10},
        is_valid=is_valid,
        validation_errors=None if is_valid else ["Invalid deck"],
        is_starter=is_starter,
        starter_type="grass" if is_starter else None,
        description="A test deck",
        created_at=datetime.now(UTC),
        updated_at=datetime.now(UTC),
    )


# =============================================================================
# GET /decks Tests
# =============================================================================


class TestListDecks:
    """Tests for GET /api/decks endpoint."""

    def test_returns_empty_list_for_new_user(
        self, client: TestClient, auth_headers, mock_deck_service
    ):
        """
        Test that endpoint returns empty list for new user.

        New users should have no decks.
        """
        mock_deck_service.get_user_decks = AsyncMock(return_value=[])

        response = client.get("/api/decks", headers=auth_headers)

        assert response.status_code == status.HTTP_200_OK
        data = response.json()
        assert data["decks"] == []
        assert data["deck_count"] == 0

    def test_returns_user_decks(self, client: TestClient, auth_headers, mock_deck_service):
        """
        Test that endpoint returns all user's decks.

        Should include deck details and count.
        """
        decks = [make_deck_entry("Deck 1"), make_deck_entry("Deck 2")]
        mock_deck_service.get_user_decks = AsyncMock(return_value=decks)

        response = client.get("/api/decks", headers=auth_headers)

        assert response.status_code == status.HTTP_200_OK
        data = response.json()
        assert len(data["decks"]) == 2
        assert data["deck_count"] == 2

    def test_includes_deck_limit_for_free_user(
        self, client: TestClient, auth_headers, mock_deck_service, test_user
    ):
        """
        Test that deck_limit is included for free users.

        Free users have a deck limit of 5.
        """
        mock_deck_service.get_user_decks = AsyncMock(return_value=[])

        response = client.get("/api/decks", headers=auth_headers)

        assert response.status_code == status.HTTP_200_OK
        data = response.json()
        assert data["deck_limit"] == 5  # Free user default

    def test_requires_authentication(self, unauthenticated_client: TestClient):
        """
        Test that endpoint requires authentication.

        Unauthenticated requests should return 401.
        """
        response = unauthenticated_client.get("/api/decks")

        assert response.status_code == status.HTTP_401_UNAUTHORIZED


# =============================================================================
# POST /decks Tests
# =============================================================================


class TestCreateDeck:
    """Tests for POST /api/decks endpoint."""

    def test_creates_deck(self, client: TestClient, auth_headers, mock_deck_service):
        """
        Test that endpoint creates a new deck.

        Should return the created deck with 201 status.
        """
        deck = make_deck_entry("My New Deck")
        mock_deck_service.create_deck = AsyncMock(return_value=deck)

        response = client.post(
            "/api/decks",
            headers=auth_headers,
            json={
                "name": "My New Deck",
                "cards": {"a1-001-bulbasaur": 4},
                "energy_cards": {"grass": 20},
            },
        )

        assert response.status_code == status.HTTP_201_CREATED
        data = response.json()
        assert data["name"] == "My New Deck"

    def test_accepts_custom_deck_config(self, client: TestClient, auth_headers, mock_deck_service):
        """
        Test that endpoint accepts custom DeckConfig from frontend.

        The backend is stateless - rules come from the request.
        """
        deck = make_deck_entry()
        mock_deck_service.create_deck = AsyncMock(return_value=deck)

        response = client.post(
            "/api/decks",
            headers=auth_headers,
            json={
                "name": "Custom Rules Deck",
                "cards": {"a1-001-bulbasaur": 10},
                "energy_cards": {"grass": 10},
                "deck_config": {
                    "min_size": 20,
                    "max_size": 20,
                    "energy_deck_size": 10,
                    "max_copies_per_card": 10,
                },
            },
        )

        assert response.status_code == status.HTTP_201_CREATED

    def test_returns_400_at_deck_limit(self, client: TestClient, auth_headers, mock_deck_service):
        """
        Test that endpoint returns 400 when at deck limit.

        Users cannot create more decks than their limit allows.
        """
        mock_deck_service.create_deck = AsyncMock(
            side_effect=DeckLimitExceededError("Deck limit reached (5/5)")
        )

        response = client.post(
            "/api/decks",
            headers=auth_headers,
            json={
                "name": "One Too Many",
                "cards": {"a1-001-bulbasaur": 4},
                "energy_cards": {"grass": 20},
            },
        )

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert "limit" in response.json()["detail"].lower()

    def test_requires_authentication(self, unauthenticated_client: TestClient):
        """
        Test that endpoint requires authentication.

        Unauthenticated requests should return 401.
        """
        response = unauthenticated_client.post(
            "/api/decks",
            json={
                "name": "Test Deck",
                "cards": {"a1-001-bulbasaur": 4},
                "energy_cards": {"grass": 20},
            },
        )

        assert response.status_code == status.HTTP_401_UNAUTHORIZED


# =============================================================================
# GET /decks/{deck_id} Tests
# =============================================================================


class TestGetDeck:
    """Tests for GET /api/decks/{deck_id} endpoint."""

    def test_returns_deck(self, client: TestClient, auth_headers, mock_deck_service):
        """
        Test that endpoint returns the requested deck.

        Should return full deck details.
        """
        deck = make_deck_entry("My Deck")
        mock_deck_service.get_deck = AsyncMock(return_value=deck)

        response = client.get(f"/api/decks/{deck.id}", headers=auth_headers)

        assert response.status_code == status.HTTP_200_OK
        data = response.json()
        assert data["name"] == "My Deck"

    def test_returns_404_for_nonexistent(self, client: TestClient, auth_headers, mock_deck_service):
        """
        Test that endpoint returns 404 for non-existent deck.

        Should return 404 when deck doesn't exist.
        """
        mock_deck_service.get_deck = AsyncMock(side_effect=DeckNotFoundError())

        response = client.get(f"/api/decks/{uuid4()}", headers=auth_headers)

        assert response.status_code == status.HTTP_404_NOT_FOUND

    def test_returns_404_for_other_user_deck(
        self, client: TestClient, auth_headers, mock_deck_service
    ):
        """
        Test that endpoint returns 404 for other user's deck.

        Users cannot access decks they don't own.
        """
        mock_deck_service.get_deck = AsyncMock(side_effect=DeckNotFoundError())

        response = client.get(f"/api/decks/{uuid4()}", headers=auth_headers)

        assert response.status_code == status.HTTP_404_NOT_FOUND

    def test_requires_authentication(self, unauthenticated_client: TestClient):
        """
        Test that endpoint requires authentication.

        Unauthenticated requests should return 401.
        """
        response = unauthenticated_client.get(f"/api/decks/{uuid4()}")

        assert response.status_code == status.HTTP_401_UNAUTHORIZED


# =============================================================================
# PUT /decks/{deck_id} Tests
# =============================================================================


class TestUpdateDeck:
    """Tests for PUT /api/decks/{deck_id} endpoint."""

    def test_updates_deck(self, client: TestClient, auth_headers, mock_deck_service):
        """
        Test that endpoint updates the deck.

        Should return the updated deck.
        """
        deck = make_deck_entry("Updated Name")
        mock_deck_service.update_deck = AsyncMock(return_value=deck)

        response = client.put(
            f"/api/decks/{deck.id}",
            headers=auth_headers,
            json={"name": "Updated Name"},
        )

        assert response.status_code == status.HTTP_200_OK
        data = response.json()
        assert data["name"] == "Updated Name"

    def test_returns_404_for_nonexistent(self, client: TestClient, auth_headers, mock_deck_service):
        """
        Test that endpoint returns 404 for non-existent deck.

        Cannot update a deck that doesn't exist.
        """
        mock_deck_service.update_deck = AsyncMock(side_effect=DeckNotFoundError())

        response = client.put(
            f"/api/decks/{uuid4()}",
            headers=auth_headers,
            json={"name": "New Name"},
        )

        assert response.status_code == status.HTTP_404_NOT_FOUND

    def test_requires_authentication(self, unauthenticated_client: TestClient):
        """
        Test that endpoint requires authentication.

        Unauthenticated requests should return 401.
        """
        response = unauthenticated_client.put(
            f"/api/decks/{uuid4()}",
            json={"name": "New Name"},
        )

        assert response.status_code == status.HTTP_401_UNAUTHORIZED


# =============================================================================
# DELETE /decks/{deck_id} Tests
# =============================================================================


class TestDeleteDeck:
    """Tests for DELETE /api/decks/{deck_id} endpoint."""

    def test_deletes_deck(self, client: TestClient, auth_headers, mock_deck_service):
        """
        Test that endpoint deletes the deck.

        Should return 204 No Content on success.
        """
        mock_deck_service.delete_deck = AsyncMock(return_value=True)

        response = client.delete(f"/api/decks/{uuid4()}", headers=auth_headers)

        assert response.status_code == status.HTTP_204_NO_CONTENT

    def test_returns_404_for_nonexistent(self, client: TestClient, auth_headers, mock_deck_service):
        """
        Test that endpoint returns 404 for non-existent deck.

        Cannot delete a deck that doesn't exist.
        """
        mock_deck_service.delete_deck = AsyncMock(side_effect=DeckNotFoundError())

        response = client.delete(f"/api/decks/{uuid4()}", headers=auth_headers)

        assert response.status_code == status.HTTP_404_NOT_FOUND

    def test_requires_authentication(self, unauthenticated_client: TestClient):
        """
        Test that endpoint requires authentication.

        Unauthenticated requests should return 401.
        """
        response = unauthenticated_client.delete(f"/api/decks/{uuid4()}")

        assert response.status_code == status.HTTP_401_UNAUTHORIZED


# =============================================================================
# POST /decks/validate Tests
# =============================================================================


class TestValidateDeck:
    """Tests for POST /api/decks/validate endpoint."""

    def test_returns_valid_result(
        self,
        client: TestClient,
        auth_headers,
        mock_collection_service,
        mock_card_service,
    ):
        """
        Test that endpoint validates deck and returns result.

        Valid decks should return is_valid=True with empty errors.
        """
        mock_collection_service.get_owned_cards_dict = AsyncMock(
            return_value={"a1-001-bulbasaur": 10}
        )

        # Mock the validate_deck function
        with patch("app.api.decks.validate_deck") as mock_validate:
            mock_validate.return_value = ValidationResult(is_valid=True, errors=[])

            with patch("app.services.card_service.get_card_service") as mock_get_cs:
                mock_get_cs.return_value = mock_card_service

                response = client.post(
                    "/api/decks/validate",
                    headers=auth_headers,
                    json={
                        "cards": {"a1-001-bulbasaur": 4},
                        "energy_cards": {"grass": 20},
                    },
                )

        assert response.status_code == status.HTTP_200_OK
        data = response.json()
        assert data["is_valid"] is True
        assert data["errors"] == []

    def test_returns_validation_errors(
        self,
        client: TestClient,
        auth_headers,
        mock_collection_service,
        mock_card_service,
    ):
        """
        Test that endpoint returns validation errors for invalid deck.

        Invalid decks should return is_valid=False with error messages.
        """
        mock_collection_service.get_owned_cards_dict = AsyncMock(return_value={})

        with patch("app.api.decks.validate_deck") as mock_validate:
            mock_validate.return_value = ValidationResult(
                is_valid=False, errors=["Deck must have 40 cards"]
            )

            with patch("app.services.card_service.get_card_service") as mock_get_cs:
                mock_get_cs.return_value = mock_card_service

                response = client.post(
                    "/api/decks/validate",
                    headers=auth_headers,
                    json={
                        "cards": {"a1-001-bulbasaur": 4},
                        "energy_cards": {"grass": 5},
                    },
                )

        assert response.status_code == status.HTTP_200_OK
        data = response.json()
        assert data["is_valid"] is False
        assert len(data["errors"]) > 0

    def test_accepts_custom_deck_config(
        self,
        client: TestClient,
        auth_headers,
        mock_collection_service,
        mock_card_service,
    ):
        """
        Test that endpoint accepts custom DeckConfig from frontend.

        Custom rules should be passed to validation.
        """
        mock_collection_service.get_owned_cards_dict = AsyncMock(return_value={})

        with patch("app.api.decks.validate_deck") as mock_validate:
            mock_validate.return_value = ValidationResult(is_valid=True, errors=[])

            with patch("app.services.card_service.get_card_service") as mock_get_cs:
                mock_get_cs.return_value = mock_card_service

                response = client.post(
                    "/api/decks/validate",
                    headers=auth_headers,
                    json={
                        "cards": {"a1-001-bulbasaur": 10},
                        "energy_cards": {"grass": 10},
                        "deck_config": {"min_size": 20, "energy_deck_size": 10},
                        "validate_ownership": False,
                    },
                )

        assert response.status_code == status.HTTP_200_OK

    def test_requires_authentication(self, unauthenticated_client: TestClient):
        """
        Test that endpoint requires authentication.

        Unauthenticated requests should return 401.
        """
        response = unauthenticated_client.post(
            "/api/decks/validate",
            json={
                "cards": {"a1-001-bulbasaur": 4},
                "energy_cards": {"grass": 20},
            },
        )

        assert response.status_code == status.HTTP_401_UNAUTHORIZED
