"""Сад-сцена (ТЗ 3.13): монеты, магазин, апгрейды, перемещение, авторазмещение."""

from typing import Any

from fastapi.testclient import TestClient

from app.services.garden import (
    DECORATIONS,
    EXPANSIONS,
    coins_for_xp,
    level_bonus_coins,
    spiral_position,
)


def _create_task(client: TestClient, **patch: Any) -> int:
    resp = client.post("/api/tasks", json={"title": "Задача для сада"})
    task_id = resp.json()["id"]
    if patch:
        resp2 = client.patch(f"/api/tasks/{task_id}", json=patch)
        assert resp2.status_code == 200
    return task_id


def _done(client: TestClient, task_id: int) -> dict[str, str]:
    resp = client.patch(f"/api/tasks/{task_id}", json={"status": "done"})
    assert resp.status_code == 200
    return {
        "xp": resp.headers.get("X-Earned-XP", ""),
        "coins": resp.headers.get("X-Earned-Coins", ""),
    }


# --- Монеты за задачу ---


def test_coins_on_done_and_idempotent(client: TestClient) -> None:
    task_id = _create_task(client)
    earned = _done(client, task_id)
    assert int(earned["coins"]) == coins_for_xp(int(earned["xp"]))

    # Повторное закрытие не добавляет ни XP, ни монет
    earned2 = _done(client, task_id)
    assert earned2["xp"] == "" and earned2["coins"] == ""

    state = client.get("/api/garden").json()
    assert state["balance"] == coins_for_xp(int(earned["xp"]))


def test_level_bonus_coins(client: TestClient) -> None:
    # Уровень 2 требует 100 XP: три тяжёлые задачи дают 3×40 = 120 → L2
    for _ in range(3):
        task_id = _create_task(client, priority=9, estimated_minutes=480)
        _done(client, task_id)
    # Монеты: 3 × 20 за задачи + бонус 25×2 = 110
    assert client.get("/api/garden").json()["balance"] == 110


# --- Перемещение ---


def test_move_and_validation(client: TestClient) -> None:
    task_id = _create_task(client)
    _done(client, task_id)
    item = next(i for i in client.get("/api/garden").json()["items"] if i["kind"] == "plant")

    resp = client.patch(f"/api/garden/items/{item['id']}", json={"x": 5, "y": 7})
    assert resp.status_code == 200
    state = client.get("/api/garden").json()
    moved = next(i for i in state["items"] if i["id"] == item["id"])
    assert (moved["x"], moved["y"]) == (5, 7)

    # Вне сетки — 400
    size = state["grid"]
    resp = client.patch(f"/api/garden/items/{item['id']}", json={"x": size["cols"], "y": 0})
    assert resp.status_code == 400


# --- Апгрейд растений ---


def test_plant_upgrade_costs_and_402(client: TestClient, monkeypatch: Any) -> None:
    # Редкость случайна — фиксируем roll: всегда common
    monkeypatch.setattr("app.services.xp.random.random", lambda: 0.99)
    task_id = _create_task(client, priority=9, estimated_minutes=480)
    _done(client, task_id)  # 40 XP → 20 монет
    plant = next(i for i in client.get("/api/garden").json()["items"] if i["kind"] == "plant")
    assert plant["rarity"] == "common"

    # Первая стадия ровно по карману (20 = 20)
    resp = client.post(f"/api/garden/plants/{plant['id']}/upgrade")
    assert resp.status_code == 200
    assert resp.json()["stage"] == 1
    assert resp.json()["balance"] == 0

    # Вторая стадия — не хватает (50 > 0)
    resp = client.post(f"/api/garden/plants/{plant['id']}/upgrade")
    assert resp.status_code == 402
    assert "50" in resp.json()["detail"]

    for _ in range(10):
        _done(client, _create_task(client))  # 10×5 монет + бонус L2 — с запасом
    resp = client.post(f"/api/garden/plants/{plant['id']}/upgrade")
    assert resp.status_code == 200
    assert resp.json()["stage"] == 2

    # Больше нельзя — 400
    resp = client.post(f"/api/garden/plants/{plant['id']}/upgrade")
    assert resp.status_code == 400


# --- Магазин ---


def test_shop_buy_decoration_and_repeatable(client: TestClient, monkeypatch: Any) -> None:
    monkeypatch.setattr("app.services.xp.random.random", lambda: 0.99)
    _done(client, _create_task(client, priority=9, estimated_minutes=480))
    _done(client, _create_task(client, priority=9, estimated_minutes=480))  # 40 монет
    resp = client.post("/api/garden/shop/buy", json={"item_key": "fence"})
    assert resp.status_code == 200
    balance_after = resp.json()["balance"]
    assert balance_after == 40 - DECORATIONS["fence"]["cost"]
    assert any(
        i["kind"] == "decoration" and i["item_key"] == "fence"
        for i in client.get("/api/garden").json()["items"]
    )


def test_shop_unknown_400_and_unique_409(client: TestClient, monkeypatch: Any) -> None:
    assert client.post("/api/garden/shop/buy", json={"item_key": "nope"}).status_code == 400

    # Уникальную декорацию нельзя купить дважды: 6 задач по 40 XP = 240 XP (L2),
    # монеты 6×20 + бонус 50 = 170 ≥ 80
    monkeypatch.setattr("app.services.xp.random.random", lambda: 0.99)
    for _ in range(6):
        _done(client, _create_task(client, priority=9, estimated_minutes=480))
    assert client.post("/api/garden/shop/buy", json={"item_key": "lantern"}).status_code == 200
    assert client.post("/api/garden/shop/buy", json={"item_key": "lantern"}).status_code == 409


def test_shop_expansion_lock_and_order(client: TestClient, monkeypatch: Any) -> None:
    # Уровень 1 < 3 — замок
    resp = client.post("/api/garden/shop/buy", json={"item_key": "expansion_1"})
    assert resp.status_code == 403
    assert "3" in resp.json()["detail"]

    # До L3 нужен 300 XP: 8 задач по 40 = 320; монеты 8×20 + бонусы 50+75 = 285
    monkeypatch.setattr("app.services.xp.random.random", lambda: 0.99)
    for _ in range(8):
        _done(client, _create_task(client, priority=9, estimated_minutes=480))
    state = client.get("/api/garden").json()
    assert state["level"] == 3

    resp = client.post("/api/garden/shop/buy", json={"item_key": "expansion_1"})
    assert resp.status_code == 200
    assert client.get("/api/garden").json()["grid"]["cols"] > 32

    # Внепорядковое расширение (expansion_2 ещё следующий, 3 просит) — 409
    resp = client.post("/api/garden/shop/buy", json={"item_key": "expansion_3"})
    assert resp.status_code == 409


# --- Авторазмещение и виды ---


def test_species_unlock_and_placement(client: TestClient) -> None:
    state = client.get("/api/garden").json()
    # Уровень 1: только стартовые виды, cactus закрыт (premium)
    keys = [s["key"] for s in state["species"] if s["unlocked"]]
    assert "ph-flower" in keys and "ph-cactus" not in keys

    _done(client, _create_task(client))
    state = client.get("/api/garden").json()
    plants = [i for i in state["items"] if i["kind"] == "plant"]
    assert len(plants) == 1
    # Размещение по спирали — вне footprint домика и в границах сетки
    assert plants[0]["item_key"] in keys
    assert 0 <= plants[0]["x"] < state["grid"]["cols"]
    assert 0 <= plants[0]["y"] < state["grid"]["rows"]


def test_spiral_position_skips_occupied() -> None:
    occupied: set[tuple[int, int]] = set()
    first = spiral_position(occupied, 32, 20)
    assert first == (13, 7)  # радиус 3 от центра — первый шаг спирали
    occupied.add(first)
    second = spiral_position(occupied, 32, 20)
    assert second not in occupied


def test_level_bonus_helper() -> None:
    assert level_bonus_coins(2) == 50
    assert coins_for_xp(40) == 20
    assert set(EXPANSIONS) == {"expansion_1", "expansion_2", "expansion_3"}
