"""Tests for the Steam Workshop API client and downloader."""
from __future__ import annotations

import os
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from l4d2web.services import steam_workshop


def test_parse_workshop_input_single_numeric() -> None:
    assert steam_workshop.parse_workshop_input("12345") == ["12345"]


def test_parse_workshop_input_single_url() -> None:
    url = "https://steamcommunity.com/sharedfiles/filedetails/?id=98765"
    assert steam_workshop.parse_workshop_input(url) == ["98765"]


def test_parse_workshop_input_workshop_url_variant() -> None:
    url = "steamcommunity.com/workshop/filedetails/?id=42"
    assert steam_workshop.parse_workshop_input(url) == ["42"]


def test_parse_workshop_input_multiline_batch() -> None:
    raw = """
    12345
    https://steamcommunity.com/sharedfiles/filedetails/?id=67890
    99999
    """
    assert steam_workshop.parse_workshop_input(raw) == ["12345", "67890", "99999"]


def test_parse_workshop_input_deduplicates_preserving_order() -> None:
    raw = "100\n200\n100\n300"
    assert steam_workshop.parse_workshop_input(raw) == ["100", "200", "300"]


def test_parse_workshop_input_rejects_garbage() -> None:
    with pytest.raises(ValueError):
        steam_workshop.parse_workshop_input("not-a-number")


def test_parse_workshop_input_rejects_empty() -> None:
    with pytest.raises(ValueError):
        steam_workshop.parse_workshop_input("")


def test_parse_workshop_input_rejects_non_steam_url() -> None:
    with pytest.raises(ValueError):
        steam_workshop.parse_workshop_input("https://example.com/?id=12345")


def test_endpoints_are_https() -> None:
    assert steam_workshop.GET_PUBLISHED_FILE_DETAILS_URL.startswith("https://")
    assert steam_workshop.GET_COLLECTION_DETAILS_URL.startswith("https://")
    assert "api.steampowered.com" in steam_workshop.GET_PUBLISHED_FILE_DETAILS_URL


def test_resolve_collection_returns_child_ids() -> None:
    fake_response = MagicMock(status_code=200)
    fake_response.raise_for_status = MagicMock()
    fake_response.json.return_value = {
        "response": {
            "collectiondetails": [
                {
                    "publishedfileid": "555",
                    "result": 1,
                    "children": [
                        {"publishedfileid": "1001", "filetype": 0},
                        {"publishedfileid": "1002", "filetype": 0},
                        {"publishedfileid": "9999", "filetype": 1},  # nested collection — skip
                    ],
                }
            ]
        }
    }
    with patch.object(steam_workshop, "_session", return_value=MagicMock(post=MagicMock(return_value=fake_response))):
        ids = steam_workshop.resolve_collection("555")
    assert ids == ["1001", "1002"]


def test_fetch_metadata_batch_parses_published_file_details() -> None:
    fake_response = MagicMock(status_code=200)
    fake_response.raise_for_status = MagicMock()
    fake_response.json.return_value = {
        "response": {
            "publishedfiledetails": [
                {
                    "publishedfileid": "1001",
                    "result": 1,
                    "consumer_app_id": 550,
                    "title": "Map A",
                    "filename": "map_a.vpk",
                    "file_url": "https://steamusercontent.com/abc/map_a.vpk",
                    "file_size": "1024",
                    "time_updated": 1700000000,
                    "preview_url": "https://steamuserimages.com/preview_a.jpg",
                }
            ]
        }
    }
    with patch.object(steam_workshop, "_session", return_value=MagicMock(post=MagicMock(return_value=fake_response))):
        metas = steam_workshop.fetch_metadata_batch(["1001"], mode="add")
    assert len(metas) == 1
    m = metas[0]
    assert m.steam_id == "1001"
    assert m.title == "Map A"
    assert m.filename == "map_a.vpk"
    assert m.file_url == "https://steamusercontent.com/abc/map_a.vpk"
    assert m.file_size == 1024
    assert m.time_updated == 1700000000
    assert m.preview_url == "https://steamuserimages.com/preview_a.jpg"
    assert m.consumer_app_id == 550
    assert m.result == 1


def test_fetch_metadata_batch_rejects_non_l4d2_in_add_mode() -> None:
    fake_response = MagicMock(status_code=200)
    fake_response.raise_for_status = MagicMock()
    fake_response.json.return_value = {
        "response": {
            "publishedfiledetails": [
                {
                    "publishedfileid": "1001",
                    "result": 1,
                    "consumer_app_id": 440,  # TF2
                    "title": "Other",
                    "filename": "x.vpk",
                    "file_url": "https://example.com/x.vpk",
                    "file_size": "0",
                    "time_updated": 0,
                }
            ]
        }
    }
    with patch.object(steam_workshop, "_session", return_value=MagicMock(post=MagicMock(return_value=fake_response))):
        with pytest.raises(steam_workshop.WorkshopValidationError):
            steam_workshop.fetch_metadata_batch(["1001"], mode="add")


def test_fetch_metadata_batch_skips_non_l4d2_in_refresh_mode() -> None:
    fake_response = MagicMock(status_code=200)
    fake_response.raise_for_status = MagicMock()
    fake_response.json.return_value = {
        "response": {
            "publishedfiledetails": [
                {
                    "publishedfileid": "1001",
                    "result": 1,
                    "consumer_app_id": 440,
                    "title": "Other",
                    "filename": "x.vpk",
                    "file_url": "https://example.com/x.vpk",
                    "file_size": "0",
                    "time_updated": 0,
                },
                {
                    "publishedfileid": "1002",
                    "result": 1,
                    "consumer_app_id": 550,
                    "title": "Good",
                    "filename": "g.vpk",
                    "file_url": "https://example.com/g.vpk",
                    "file_size": "100",
                    "time_updated": 1,
                },
            ]
        }
    }
    with patch.object(steam_workshop, "_session", return_value=MagicMock(post=MagicMock(return_value=fake_response))):
        metas = steam_workshop.fetch_metadata_batch(["1001", "1002"], mode="refresh")
    # The non-L4D2 item is dropped; the L4D2 item is kept.
    assert [m.steam_id for m in metas] == ["1002"]


def test_fetch_metadata_batch_captures_result_failure() -> None:
    fake_response = MagicMock(status_code=200)
    fake_response.raise_for_status = MagicMock()
    fake_response.json.return_value = {
        "response": {
            "publishedfiledetails": [
                {
                    "publishedfileid": "999",
                    "result": 9,  # not found / hidden / etc.
                }
            ]
        }
    }
    with patch.object(steam_workshop, "_session", return_value=MagicMock(post=MagicMock(return_value=fake_response))):
        metas = steam_workshop.fetch_metadata_batch(["999"], mode="refresh")
    # Item is kept but marked with the failing result; consumer app id never validated.
    assert len(metas) == 1
    assert metas[0].result == 9


def test_download_to_cache_writes_atomically_and_sets_mtime(tmp_path: Path) -> None:
    cache_root = tmp_path / "workshop_cache"
    cache_root.mkdir()
    meta = steam_workshop.WorkshopMetadata(
        steam_id="1001",
        title="A",
        filename="a.vpk",
        file_url="https://example.com/a.vpk",
        file_size=11,
        time_updated=1700000000,
        preview_url="",
        consumer_app_id=550,
        result=1,
    )
    fake_response = MagicMock(status_code=200)
    fake_response.raise_for_status = MagicMock()
    fake_response.iter_content.return_value = [b"hello world"]

    with patch.object(steam_workshop, "_session", return_value=MagicMock(get=MagicMock(return_value=fake_response))):
        path = steam_workshop.download_to_cache(meta, cache_root)

    assert path == cache_root / "1001.vpk"
    assert path.read_bytes() == b"hello world"
    assert int(path.stat().st_mtime) == 1700000000
    # No leftover .partial file.
    assert not (cache_root / "1001.vpk.partial").exists()


def test_download_to_cache_is_idempotent(tmp_path: Path) -> None:
    cache_root = tmp_path / "workshop_cache"
    cache_root.mkdir()
    target = cache_root / "1001.vpk"
    target.write_bytes(b"existing")
    os.utime(target, (1700000000, 1700000000))

    meta = steam_workshop.WorkshopMetadata(
        steam_id="1001",
        title="A",
        filename="a.vpk",
        file_url="https://example.com/a.vpk",
        file_size=8,  # matches existing
        time_updated=1700000000,  # matches existing mtime
        preview_url="",
        consumer_app_id=550,
        result=1,
    )

    fake_session = MagicMock()
    with patch.object(steam_workshop, "_session", return_value=fake_session):
        steam_workshop.download_to_cache(meta, cache_root)

    fake_session.get.assert_not_called()


def test_download_to_cache_redownloads_when_mtime_or_size_differ(tmp_path: Path) -> None:
    cache_root = tmp_path / "workshop_cache"
    cache_root.mkdir()
    target = cache_root / "1001.vpk"
    target.write_bytes(b"old")
    os.utime(target, (1500000000, 1500000000))

    meta = steam_workshop.WorkshopMetadata(
        steam_id="1001",
        title="A",
        filename="a.vpk",
        file_url="https://example.com/a.vpk",
        file_size=11,
        time_updated=1700000000,
        preview_url="",
        consumer_app_id=550,
        result=1,
    )

    fake_response = MagicMock(status_code=200)
    fake_response.raise_for_status = MagicMock()
    fake_response.iter_content.return_value = [b"hello world"]

    with patch.object(steam_workshop, "_session", return_value=MagicMock(get=MagicMock(return_value=fake_response))):
        steam_workshop.download_to_cache(meta, cache_root)

    assert target.read_bytes() == b"hello world"
    assert int(target.stat().st_mtime) == 1700000000


def test_refresh_all_uses_thread_pool_and_collects_errors(tmp_path: Path) -> None:
    cache_root = tmp_path / "workshop_cache"
    cache_root.mkdir()

    metas = [
        steam_workshop.WorkshopMetadata(
            steam_id=str(i),
            title=f"M{i}",
            filename=f"m{i}.vpk",
            file_url=f"https://example.com/m{i}.vpk",
            file_size=5,
            time_updated=1700000000,
            preview_url="",
            consumer_app_id=550,
            result=1,
        )
        for i in (1, 2, 3)
    ]

    def fake_download(meta, cache_root_arg, **kwargs):
        if meta.steam_id == "2":
            raise RuntimeError("simulated download failure")
        return cache_root_arg / f"{meta.steam_id}.vpk"

    with patch.object(steam_workshop, "download_to_cache", side_effect=fake_download):
        report = steam_workshop.refresh_all(metas, cache_root, executor_workers=4)

    assert report.downloaded == 2
    assert report.errors == 1
    assert "2" in report.per_item_errors
