"""
End-to-end user journey tests for Ohm Stream Downloader.

These tests verify complete user workflows including authentication,
search, settings management, and download operations.

All tests use mocked providers and in-memory SQLite — no real network calls.
"""

import os
import time
import uuid

# FORCE DATABASE_URL to in-memory for ALL tests before ANY app imports
os.environ["DATABASE_URL"] = "sqlite://"

import pytest
from typing import Dict, List, Optional, Tuple
from unittest.mock import AsyncMock, Mock, patch
from fastapi.testclient import TestClient

from main import app


# =============================================================================
# MOCK DATA CONSTANTS
# =============================================================================

MOCK_ANIME_SEARCH_RESULTS: Dict[str, List[Dict]] = {
    "anime-sama": [
        {
            "title": "Naruto Shippuden",
            "url": "https://anime-sama.si/catalogue/naruto/saison1/vostfr/",
            "cover_image": "https://example.com/naruto.jpg",
            "type": "search_result",
        },
        {
            "title": "One Piece",
            "url": "https://anime-sama.si/catalogue/one-piece/saison1/vostfr/",
            "cover_image": "https://example.com/onepiece.jpg",
            "type": "search_result",
        },
    ],
    "anime-ultime": [
        {
            "title": "Naruto Shippuden",
            "url": "https://www.anime-ultime.net/naruto-shippuden",
            "cover_image": "https://example.com/naruto-au.jpg",
            "type": "search_result",
        },
    ],
}

MOCK_SERIES_SEARCH_RESULTS: Dict[str, Dict] = {
    "Breaking Bad": {
        "base_name": "Breaking Bad",
        "cover": "https://example.com/bb.jpg",
        "synopsis": "A chemistry teacher turns to manufacturing methamphetamine...",
        "seasons": {
            1: [
                {
                    "id": "fs7",
                    "url": "https://fs7.fr/breaking-bad/saison-1",
                    "provider_id": "fs7",
                }
            ],
            2: [
                {
                    "id": "fs7",
                    "url": "https://fs7.fr/breaking-bad/saison-2",
                    "provider_id": "fs7",
                }
            ],
        },
    }
}

MOCK_EPISODE_LIST: List[Dict] = [
    {
        "episode": 1,
        "url": "https://example.com/video1.mp4|https://anime-sama.si/catalogue/naruto/s1/ep1|Naruto - Episode 1",
    },
    {
        "episode": 2,
        "url": "https://example.com/video2.mp4|https://anime-sama.si/catalogue/naruto/s1/ep2|Naruto - Episode 2",
    },
    {
        "episode": 3,
        "url": "https://example.com/video3.mp4|https://anime-sama.si/catalogue/naruto/s1/ep3|Naruto - Episode 3",
    },
    {
        "episode": 4,
        "url": "https://example.com/video4.mp4|https://anime-sama.si/catalogue/naruto/s1/ep4|Naruto - Episode 4",
    },
    {
        "episode": 5,
        "url": "https://example.com/video5.mp4|https://anime-sama.si/catalogue/naruto/s1/ep5|Naruto - Episode 5",
    },
]

MOCK_DOWNLOAD_LINK: Tuple[str, str] = (
    "https://example.com/video1.mp4",
    "Naruto_Episode_1.mp4",
)


# =============================================================================
# FIXTURES
# =============================================================================


@pytest.fixture
def journey_client():
    """
    Create a TestClient with a registered and logged-in user.

    Uses 'with' context manager to ensure startup events fire
    and database tables are properly initialized.
    """
    unique_id = f"{int(time.time())}_{uuid.uuid4().hex[:8]}"
    test_username = f"journey_user_{unique_id}"
    test_password = "TestPassword123!"
    test_email = f"{test_username}@test.example.com"

    with TestClient(app) as client:
        register_response = client.post(
            "/api/auth/register",
            json={
                "username": test_username,
                "password": test_password,
                "email": test_email,
            },
        )
        assert register_response.status_code == 200, (
            f"Registration failed: {register_response.json()}"
        )

        login_response = client.post(
            "/api/auth/login",
            json={
                "username": test_username,
                "password": test_password,
            },
        )
        assert login_response.status_code == 200, (
            f"Login failed: {login_response.json()}"
        )

        access_token = login_response.json()["access_token"]
        client.headers["Authorization"] = f"Bearer {access_token}"

        yield client

    app.dependency_overrides.clear()


@pytest.fixture
def unauth_client():
    """Create a plain TestClient without authentication."""
    with TestClient(app) as client:
        yield client


# =============================================================================
# TEST CLASSES
# =============================================================================


class TestAuthJourney:
    """Tests for the authentication user journey — registration, login, tokens."""

    def test_register_new_user(self):
        unique_id = f"{int(time.time())}_{uuid.uuid4().hex[:8]}"
        username = f"auth_test_{unique_id}"
        with TestClient(app) as client:
            response = client.post(
                "/api/auth/register",
                json={"username": username, "password": "SecurePass123!"},
            )
        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "success"
        assert data["user"]["username"] == username
        assert "id" in data["user"]

    def test_register_duplicate_user(self):
        username = f"dup_{uuid.uuid4().hex[:8]}"
        with TestClient(app) as client:
            first = client.post(
                "/api/auth/register",
                json={"username": username, "password": "SecurePass123!"},
            )
            assert first.status_code == 200

            second = client.post(
                "/api/auth/register",
                json={"username": username, "password": "SecurePass123!"},
            )
            assert second.status_code in (400, 500)

    def test_login_success(self):
        unique_id = f"{int(time.time())}_{uuid.uuid4().hex[:8]}"
        username = f"login_test_{unique_id}"
        password = "SecurePass123!"

        with TestClient(app) as client:
            client.post(
                "/api/auth/register",
                json={"username": username, "password": password},
            )
            response = client.post(
                "/api/auth/login",
                json={"username": username, "password": password},
            )
        assert response.status_code == 200
        data = response.json()
        assert "access_token" in data
        assert data["token_type"] == "bearer"
        assert data["user"]["username"] == username

    def test_login_wrong_password(self):
        unique_id = f"{int(time.time())}_{uuid.uuid4().hex[:8]}"
        username = f"wrong_pw_{unique_id}"

        with TestClient(app) as client:
            client.post(
                "/api/auth/register",
                json={"username": username, "password": "CorrectPass123!"},
            )

        with TestClient(app) as client:
            response = client.post(
                "/api/auth/login",
                json={"username": username, "password": "WrongPass999!"},
            )
        assert response.status_code == 401

    def test_get_current_user(self, journey_client):
        response = journey_client.get("/api/auth/me")
        assert response.status_code == 200
        data = response.json()
        assert "user" in data
        assert "username" in data["user"]
        assert "id" in data["user"]

    def test_unauthorized_access(self):
        with TestClient(app) as client:
            response = client.get("/api/auth/me")
        assert response.status_code == 403


class TestSearchJourney:
    """Tests for the search user journey — anime/series search, episodes, metadata."""

    @patch("app.routers.router_anime.providers_manager")
    @patch("app.routers.router_anime.get_metadata_enricher")
    def test_search_anime(self, mock_enricher, mock_pm, journey_client):
        mock_provider = Mock()
        mock_provider.id = "anime-sama"
        mock_provider.search = AsyncMock(
            return_value=[
                Mock(
                    model_dump=Mock(
                        return_value={
                            "title": "Naruto Shippuden",
                            "url": "https://anime-sama.si/catalogue/naruto/s1/vostfr/",
                            "cover_image": "https://example.com/naruto.jpg",
                            "type": "search_result",
                        }
                    )
                ),
            ]
        )
        mock_pm.get_active_providers.return_value = [mock_provider]

        mock_meta = AsyncMock()
        mock_meta.enrich_metadata = AsyncMock(return_value=None)
        mock_enricher.return_value = mock_meta

        response = journey_client.get("/api/anime/search?q=naruto")
        assert response.status_code == 200
        data = response.json()
        assert data["query"] == "naruto"
        assert "results" in data

    @patch("app.routers.router_anime.providers_manager")
    @patch("app.routers.router_anime.get_metadata_enricher")
    def test_search_anime_results_have_providers(
        self, mock_enricher, mock_pm, journey_client
    ):
        mock_provider = Mock()
        mock_provider.id = "anime-sama"
        mock_provider.search = AsyncMock(
            return_value=[
                Mock(
                    model_dump=Mock(
                        return_value={
                            "title": "Naruto Shippuden",
                            "url": "https://anime-sama.si/catalogue/naruto/s1/vostfr/",
                            "cover_image": "https://example.com/naruto.jpg",
                            "type": "search_result",
                        }
                    )
                ),
            ]
        )
        mock_pm.get_active_providers.return_value = [mock_provider]

        mock_meta = AsyncMock()
        mock_meta.enrich_metadata = AsyncMock(return_value=None)
        mock_enricher.return_value = mock_meta

        response = journey_client.get("/api/anime/search?q=naruto")
        data = response.json()
        assert "anime-sama" in data["results"]

    @patch("app.routers.router_anime.get_series_providers")
    @patch("app.routers.router_anime.get_metadata_enricher")
    def test_search_series(self, mock_enricher, mock_series_providers, journey_client):
        mock_series_providers.return_value = {"fs7": {"name": "FS7"}}

        mock_meta = AsyncMock()
        mock_meta.enrich_metadata = AsyncMock(return_value=None)
        mock_enricher.return_value = mock_meta

        mock_fs7_instance = Mock()
        mock_fs7_instance.search_anime = AsyncMock(
            return_value=[
                {
                    "title": "Breaking Bad",
                    "url": "https://fs7.fr/breaking-bad/saison-1",
                    "cover_image": "https://example.com/bb.jpg",
                }
            ]
        )

        with patch.dict(
            "sys.modules",
            {
                "app.downloaders.series_sites.fs7": Mock(
                    FS7Downloader=Mock(return_value=mock_fs7_instance)
                )
            },
        ):
            response = journey_client.get("/api/series/search?q=breaking+bad")
        assert response.status_code == 200
        data = response.json()
        assert data["query"] == "breaking bad"
        assert "results" in data

    @patch("app.routers.router_anime.providers_manager")
    @patch("app.routers.router_anime.get_metadata_enricher")
    def test_search_anime_no_results(self, mock_enricher, mock_pm, journey_client):
        mock_provider = Mock()
        mock_provider.id = "anime-sama"
        mock_provider.search = AsyncMock(return_value=[])
        mock_pm.get_active_providers.return_value = [mock_provider]

        mock_meta = AsyncMock()
        mock_meta.enrich_metadata = AsyncMock(return_value=None)
        mock_enricher.return_value = mock_meta

        response = journey_client.get("/api/anime/search?q=test_no_results_xyz")
        assert response.status_code == 200
        data = response.json()
        assert "results" in data
        assert "anime-sama" not in data["results"]

    @patch("app.routers.router_anime.get_downloader")
    def test_get_episodes(self, mock_get_dl):
        mock_dl = Mock()
        mock_dl.get_episodes = AsyncMock(return_value=MOCK_EPISODE_LIST)
        mock_get_dl.return_value = mock_dl

        with TestClient(app) as client:
            response = client.get(
                "/api/anime/episodes?url=https://anime-sama.si/test&lang=vostfr"
            )
        assert response.status_code == 200
        data = response.json()
        assert data["lang"] == "vostfr"
        assert len(data["episodes"]) == 5
        assert data["episodes"][0]["episode"] == 1

    @patch("app.routers.router_anime.get_downloader")
    def test_get_anime_metadata(self, mock_get_dl):
        mock_dl = Mock()
        mock_dl.get_anime_metadata = AsyncMock(
            return_value={
                "synopsis": "A ninja story",
                "genres": ["Action", "Adventure"],
                "rating": "8.5/10",
            }
        )
        mock_dl.hasattr = Mock(return_value=True)
        mock_get_dl.return_value = mock_dl

        with TestClient(app) as client:
            response = client.get(
                "/api/anime/metadata?url=https://anime-sama.si/naruto"
            )
        assert response.status_code == 200
        data = response.json()
        assert data["url"] == "https://anime-sama.si/naruto"
        assert data["metadata"]["synopsis"] == "A ninja story"


class TestSettingsJourney:
    """Tests for the settings management user journey — preferences, providers, theme."""

    def test_get_default_settings(self, journey_client):
        response = journey_client.get("/api/settings")
        assert response.status_code == 200
        data = response.json()
        assert data["default_lang"] == "vostfr"
        assert data["theme"] == "dark"

    def test_update_lang(self, journey_client):
        response = journey_client.patch("/api/settings", json={"default_lang": "vf"})
        assert response.status_code == 200
        data = response.json()
        assert data["default_lang"] == "vf"

        verify = journey_client.get("/api/settings")
        assert verify.json()["default_lang"] == "vf"

    def test_update_theme(self, journey_client):
        response = journey_client.patch("/api/settings", json={"theme": "oled"})
        assert response.status_code == 200
        assert response.json()["theme"] == "oled"

    def test_settings_persist_across_requests(self, journey_client):
        journey_client.patch(
            "/api/settings",
            json={
                "default_lang": "vf",
                "theme": "oled",
            },
        )

        first = journey_client.get("/api/settings").json()
        second = journey_client.get("/api/settings").json()

        assert first["default_lang"] == "vf"
        assert first["theme"] == "oled"
        assert second["default_lang"] == first["default_lang"]
        assert second["theme"] == first["theme"]

    @patch("app.routers.router_settings.providers_manager")
    def test_get_providers_availability(self, mock_pm, journey_client):
        mock_pm.get_all_status.return_value = {
            "anime-sama": {"status": "up"},
            "fs7": {"status": "up"},
        }

        response = journey_client.get("/api/settings/providers/availability")
        assert response.status_code == 200
        data = response.json()
        assert isinstance(data, list)
        provider_ids = {p["id"] for p in data}
        assert len(provider_ids) > 0

    @patch("app.routers.router_settings.providers_manager")
    def test_toggle_provider(self, mock_pm, journey_client):
        mock_pm.get_all_status.return_value = {}

        toggle = journey_client.post("/api/settings/providers/anime-sama/toggle")
        assert toggle.status_code == 200
        data = toggle.json()
        assert data["id"] == "anime-sama"
        assert data["enabled"] is False

        toggle_back = journey_client.post("/api/settings/providers/anime-sama/toggle")
        assert toggle_back.json()["enabled"] is True

        settings = journey_client.get("/api/settings").json()
        assert "anime-sama" not in settings["disabled_providers"]


class TestDownloadJourney:
    """Tests for the download management user journey — create, list, status, cancel."""

    @patch("app.routers.router_anime.get_download_manager")
    def test_create_single_download(self, mock_get_dm, journey_client):
        import tempfile
        from pathlib import Path
        from app.download_manager import DownloadManager

        tmp_dir = Path(tempfile.mkdtemp()) / "downloads"
        tmp_dir.mkdir(exist_ok=True)
        manager = DownloadManager(download_dir=str(tmp_dir), max_parallel=1)
        mock_get_dm.return_value = manager

        response = journey_client.post(
            "/api/anime/download?url=https://example.com/video.mp4"
        )
        assert response.status_code == 200
        data = response.json()
        assert "task_id" in data

    @patch("app.routers.router_downloads.get_download_manager")
    def test_list_downloads(self, mock_get_dm, journey_client):
        import tempfile
        from pathlib import Path
        from app.download_manager import DownloadManager

        tmp_dir = Path(tempfile.mkdtemp()) / "downloads"
        tmp_dir.mkdir(exist_ok=True)
        manager = DownloadManager(download_dir=str(tmp_dir), max_parallel=1)
        mock_get_dm.return_value = manager

        journey_client.post(
            "/api/downloads",
            json={"url": "https://example.com/video1.mp4"},
        )
        response = journey_client.get("/api/downloads")
        assert response.status_code == 200
        data = response.json()
        assert "downloads" in data
        assert len(data["downloads"]) >= 1

    @patch("app.routers.router_downloads.get_download_manager")
    def test_download_task_status(self, mock_get_dm, journey_client):
        import tempfile
        from pathlib import Path
        from app.download_manager import DownloadManager

        tmp_dir = Path(tempfile.mkdtemp()) / "downloads"
        tmp_dir.mkdir(exist_ok=True)
        manager = DownloadManager(download_dir=str(tmp_dir), max_parallel=1)
        mock_get_dm.return_value = manager

        task_resp = journey_client.post(
            "/api/downloads",
            json={"url": "https://example.com/status_test.mp4"},
        )
        task_id = task_resp.json()["id"]

        status_resp = journey_client.get(f"/api/downloads/{task_id}")
        assert status_resp.status_code == 200
        assert status_resp.json()["id"] == task_id

    @patch("app.routers.router_downloads.get_download_manager")
    def test_cancel_download(self, mock_get_dm, journey_client):
        import tempfile
        from pathlib import Path
        from app.download_manager import DownloadManager

        tmp_dir = Path(tempfile.mkdtemp()) / "downloads"
        tmp_dir.mkdir(exist_ok=True)
        manager = DownloadManager(download_dir=str(tmp_dir), max_parallel=1)
        mock_get_dm.return_value = manager

        task_resp = journey_client.post(
            "/api/downloads",
            json={"url": "https://example.com/cancel_test.mp4"},
        )
        assert task_resp.status_code == 200
        task_id = task_resp.json()["id"]

        cancel_resp = journey_client.delete(f"/api/downloads/{task_id}")
        assert cancel_resp.status_code == 200

    @patch("app.routers.router_anime.get_download_manager")
    @patch("app.routers.router_anime.get_downloader")
    def test_download_season(self, mock_get_dl, mock_get_dm, journey_client):
        import tempfile
        from pathlib import Path
        from app.download_manager import DownloadManager

        mock_dl = Mock()
        mock_dl.get_episodes = AsyncMock(return_value=MOCK_EPISODE_LIST)
        mock_get_dl.return_value = mock_dl

        tmp_dir = Path(tempfile.mkdtemp()) / "downloads"
        tmp_dir.mkdir(exist_ok=True)
        manager = DownloadManager(download_dir=str(tmp_dir), max_parallel=1)
        mock_get_dm.return_value = manager
        mock_get_dm.return_value = manager

        response = journey_client.post(
            "/api/anime/download-season?url=https://anime-sama.si/test&lang=vostfr"
        )
        assert response.status_code == 200
        data = response.json()
        assert data["total_episodes"] == 5
        assert len(data["task_ids"]) == 5
