# retoor <retoor@molodetz.nl>

from datetime import datetime, timedelta, timezone

import pytest

from devplacepy.database import get_table
from devplacepy.services.game import economy, store
from devplacepy.services.game.store import GameError


def _user(username):
    users = get_table("users")
    row = users.find_one(username=username)
    if not row:
        users.insert(
            {
                "uid": f"uid-{username}",
                "username": username,
                "terms_version": "1",
                "role": "Member",
                "created_at": datetime.now(timezone.utc).isoformat(),
            }
        )
        row = users.find_one(username=username)
    return row


def _clear_game_caches():
    from devplacepy.services.game.store import farm as farm_store
    from devplacepy.services.game.store import market as market_store

    farm_store._leaderboard_cache.clear()
    farm_store._board_cache.clear()
    market_store._saturation_cache.clear()


def _reset(username, coins=100000):
    user = _user(username)
    farm = store.get_farm(user["uid"])
    if farm:
        get_table("game_plots").delete(farm_uid=farm["uid"])
        get_table("game_quests").delete(farm_uid=farm["uid"])
        get_table("game_farms").delete(uid=farm["uid"])
    get_table("game_steals").delete(thief_uid=user["uid"])
    get_table("game_steals").delete(owner_uid=user["uid"])
    get_table("game_cosmetics").delete(user_uid=user["uid"])
    get_table("game_market_ticks").delete()
    get_table("game_treasury").delete()
    _clear_game_caches()
    farm = store.ensure_farm(user["uid"])
    get_table("game_farms").update({"uid": farm["uid"], "coins": coins}, ["uid"])
    return user


def _set(user, **fields):
    farm = store.get_farm(user["uid"])
    get_table("game_farms").update({"uid": farm["uid"], **fields}, ["uid"])


def _warp(user):
    farm = store.get_farm(user["uid"])
    past = (datetime.now(timezone.utc) - timedelta(seconds=5)).isoformat()
    for plot in store.get_plots(farm["uid"]):
        if plot.get("crop_key"):
            get_table("game_plots").update(
                {"uid": plot["uid"], "ready_at": past}, ["uid"]
            )


def _remaining(user, slot=0):
    farm = store.get_farm(user["uid"])
    data = store.serialize_farm(farm, viewer=user, owner=user)
    return data["plots"][slot]["remaining_seconds"]


def _coins(user):
    return int(store.get_farm(user["uid"])["coins"])


# --- ensure / starting state -------------------------------------------------


def test_ensure_farm_starting_state(local_db):
    user = _reset("unit_a", coins=economy.STARTING_COINS)
    farm = store.get_farm(user["uid"])
    assert farm["plot_count"] == economy.STARTING_PLOTS
    plots = store.get_plots(farm["uid"])
    assert len(plots) == economy.STARTING_PLOTS
    assert all(not plot["crop_key"] for plot in plots)


def test_ensure_farm_idempotent(local_db):
    user = _reset("unit_a")
    first = store.ensure_farm(user["uid"])
    second = store.ensure_farm(user["uid"])
    assert first["uid"] == second["uid"]
    assert len(store.get_plots(first["uid"])) == economy.STARTING_PLOTS


# --- plant -------------------------------------------------------------------


def test_plant_success_deducts_cost(local_db):
    user = _reset("unit_a")
    result = store.plant(user, 0, "shell")
    assert result["spent"] == economy.crop_for("shell").cost
    assert _coins(user) == 100000 - economy.crop_for("shell").cost
    plot = store.get_plots(store.get_farm(user["uid"])["uid"])[0]
    assert plot["crop_key"] == "shell"


def test_plant_insufficient_coins(local_db):
    user = _reset("unit_a", coins=0)
    with pytest.raises(GameError):
        store.plant(user, 0, "shell")


def test_plant_occupied_plot(local_db):
    user = _reset("unit_a")
    store.plant(user, 0, "shell")
    with pytest.raises(GameError):
        store.plant(user, 0, "python")


def test_plant_locked_crop(local_db):
    user = _reset("unit_a")
    with pytest.raises(GameError):
        store.plant(user, 0, "rust")


def test_plant_unknown_crop(local_db):
    user = _reset("unit_a")
    with pytest.raises(GameError):
        store.plant(user, 0, "does_not_exist")


def test_plant_nonexistent_plot(local_db):
    user = _reset("unit_a")
    with pytest.raises(GameError):
        store.plant(user, 99, "shell")


def test_discount_perk_lowers_plant_cost(local_db):
    user = _reset("unit_a")
    _set(user, perk_discount=3)
    result = store.plant(user, 0, "python")
    crop = economy.crop_for("python")
    assert result["spent"] == economy.effective_plant_cost(crop, 3)
    assert result["spent"] < crop.cost


def test_growth_perk_shortens_build(local_db):
    plain = _reset("unit_a")
    store.plant(plain, 0, "python")
    base = _remaining(plain)
    boosted = _reset("unit_b")
    _set(boosted, perk_growth=8)
    store.plant(boosted, 0, "python")
    assert _remaining(boosted) < base


# --- harvest -----------------------------------------------------------------


def test_harvest_success_awards_coins_and_xp(local_db):
    user = _reset("unit_a")
    store.plant(user, 0, "shell")
    _warp(user)
    before = _coins(user)
    result = store.harvest(user, 0)
    crop = economy.crop_for("shell")
    expected = economy.realizable_harvest_coins(crop, golden=result["golden"])
    assert result["coins"] == expected
    assert result["xp"] == crop.reward_xp
    assert _coins(user) == before + expected
    assert store.get_farm(user["uid"])["total_harvests"] == 1


def test_harvest_empty_plot(local_db):
    user = _reset("unit_a")
    with pytest.raises(GameError):
        store.harvest(user, 0)


def test_harvest_unripe_build(local_db):
    user = _reset("unit_a")
    store.plant(user, 0, "python")
    with pytest.raises(GameError):
        store.harvest(user, 0)


def test_yield_perk_and_prestige_boost_harvest_coins(local_db):
    user = _reset("unit_a")
    _set(user, perk_yield=4, prestige=1)
    store.plant(user, 0, "shell")
    _warp(user)
    result = store.harvest(user, 0)
    crop = economy.crop_for("shell")
    assert result["coins"] == economy.realizable_harvest_coins(
        crop, 4, 1, golden=result["golden"]
    )
    assert result["coins"] > crop.reward_coins


def test_xp_perk_boosts_harvest_xp(local_db):
    user = _reset("unit_a")
    _set(user, perk_xp=10)
    store.plant(user, 0, "shell")
    _warp(user)
    result = store.harvest(user, 0)
    crop = economy.crop_for("shell")
    assert result["xp"] == economy.effective_reward_xp(crop, 10)
    assert result["xp"] > crop.reward_xp


# --- water -------------------------------------------------------------------


def test_water_rewards_visitor_and_shortens_build(local_db):
    owner = _reset("unit_a")
    visitor = _reset("unit_b")
    store.plant(owner, 0, "python")
    before_remaining = _remaining(owner)
    before_coins = _coins(visitor)
    result = store.water(visitor, owner, 0)
    assert result["reward_coins"] == economy.WATER_REWARD_COINS
    assert _coins(visitor) == before_coins + economy.WATER_REWARD_COINS
    assert _remaining(owner) < before_remaining


def test_water_own_build_blocked(local_db):
    owner = _reset("unit_a")
    store.plant(owner, 0, "python")
    with pytest.raises(GameError):
        store.water(owner, owner, 0)


def test_water_twice_blocked(local_db):
    owner = _reset("unit_a")
    visitor = _reset("unit_b")
    store.plant(owner, 0, "python")
    store.water(visitor, owner, 0)
    with pytest.raises(GameError):
        store.water(visitor, owner, 0)


def test_water_empty_plot_blocked(local_db):
    owner = _reset("unit_a")
    visitor = _reset("unit_b")
    with pytest.raises(GameError):
        store.water(visitor, owner, 0)


def test_water_maxed_blocked(local_db):
    owner = _reset("unit_a")
    store.plant(owner, 0, "python")
    for index in range(economy.MAX_WATERS_PER_PLOT):
        store.water(_reset(f"unit_w{index}"), owner, 0)
    with pytest.raises(GameError):
        store.water(_reset("unit_w_extra"), owner, 0)


# --- buy plot / upgrade CI ---------------------------------------------------


def test_buy_plot_success(local_db):
    user = _reset("unit_a")
    result = store.buy_plot(user)
    assert result["plot_count"] == economy.STARTING_PLOTS + 1
    assert len(store.get_plots(store.get_farm(user["uid"])["uid"])) == economy.STARTING_PLOTS + 1


def test_buy_plot_at_max_blocked(local_db):
    user = _reset("unit_a")
    _set(user, plot_count=economy.MAX_PLOTS)
    with pytest.raises(GameError):
        store.buy_plot(user)


def test_buy_plot_insufficient_coins(local_db):
    user = _reset("unit_a", coins=0)
    with pytest.raises(GameError):
        store.buy_plot(user)


def test_upgrade_ci_success(local_db):
    user = _reset("unit_a")
    assert store.upgrade_ci(user)["ci_tier"] == 2


def test_upgrade_ci_at_top_blocked(local_db):
    user = _reset("unit_a")
    _set(user, ci_tier=economy.MAX_CI_TIER)
    with pytest.raises(GameError):
        store.upgrade_ci(user)


def test_upgrade_ci_insufficient_coins(local_db):
    user = _reset("unit_a", coins=0)
    with pytest.raises(GameError):
        store.upgrade_ci(user)


# --- perks -------------------------------------------------------------------


def test_upgrade_perk_success(local_db):
    user = _reset("unit_a")
    result = store.upgrade_perk(user, "growth")
    assert result["level"] == 1
    assert store.get_farm(user["uid"])["perk_growth"] == 1


def test_upgrade_perk_unknown(local_db):
    user = _reset("unit_a")
    with pytest.raises(GameError):
        store.upgrade_perk(user, "telepathy")


def test_upgrade_perk_maxed(local_db):
    user = _reset("unit_a")
    _set(user, perk_growth=economy.perk_for("growth").max_level)
    with pytest.raises(GameError):
        store.upgrade_perk(user, "growth")


def test_upgrade_perk_insufficient_coins(local_db):
    user = _reset("unit_a", coins=0)
    with pytest.raises(GameError):
        store.upgrade_perk(user, "growth")


# --- daily bonus -------------------------------------------------------------


def test_daily_claim_first_time(local_db):
    user = _reset("unit_a")
    result = store.claim_daily(user)
    assert result["streak"] == 1
    assert result["reward"] == economy.daily_reward(1)


def test_daily_double_claim_blocked(local_db):
    user = _reset("unit_a")
    store.claim_daily(user)
    with pytest.raises(GameError):
        store.claim_daily(user)


def test_daily_streak_increments_consecutive_day(local_db):
    user = _reset("unit_a")
    yesterday = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
    _set(user, streak=3, last_daily_at=yesterday)
    assert store.claim_daily(user)["streak"] == 4


def test_daily_streak_resets_after_gap(local_db):
    user = _reset("unit_a")
    two_days_ago = (datetime.now(timezone.utc) - timedelta(days=2)).isoformat()
    _set(user, streak=5, last_daily_at=two_days_ago)
    assert store.claim_daily(user)["streak"] == 1


# --- quests ------------------------------------------------------------------


def _insert_quest(user, kind, goal, progress=0, claimed=0, reward_coins=50, reward_xp=5):
    farm = store.get_farm(user["uid"])
    get_table("game_quests").delete(farm_uid=farm["uid"])
    get_table("game_quests").insert(
        {
            "uid": f"q-{user['uid']}-{kind}",
            "farm_uid": farm["uid"],
            "user_uid": user["uid"],
            "day": store._today(),
            "slot_index": 0,
            "scope": "daily",
            "kind": kind,
            "label": f"{kind} {goal}",
            "goal": goal,
            "progress": progress,
            "reward_coins": reward_coins,
            "reward_xp": reward_xp,
            "claimed": claimed,
            "created_at": store._today(),
            "updated_at": store._today(),
        }
    )
    return farm


def test_ensure_quests_creates_three(local_db):
    user = _reset("unit_a")
    farm = store.get_farm(user["uid"])
    store.ensure_quests(farm, store._today())
    rows = list(get_table("game_quests").find(farm_uid=farm["uid"], day=store._today()))
    assert len(rows) == economy.DAILY_QUEST_COUNT


def test_advance_quests_increments_and_caps(local_db):
    user = _reset("unit_a")
    _insert_quest(user, "plant", goal=3)
    store.advance_quests(user["uid"], "plant", 2)
    farm = store.get_farm(user["uid"])
    row = get_table("game_quests").find_one(farm_uid=farm["uid"], kind="plant")
    assert row["progress"] == 2
    store.advance_quests(user["uid"], "plant", 5)
    row = get_table("game_quests").find_one(farm_uid=farm["uid"], kind="plant")
    assert row["progress"] == 3


def test_plant_advances_plant_quest(local_db):
    user = _reset("unit_a")
    _insert_quest(user, "plant", goal=5)
    store.plant(user, 0, "shell")
    farm = store.get_farm(user["uid"])
    row = get_table("game_quests").find_one(farm_uid=farm["uid"], kind="plant")
    assert row["progress"] == 1


def test_harvest_advances_earn_quest_by_coins(local_db):
    user = _reset("unit_a")
    _insert_quest(user, "earn", goal=1000)
    store.plant(user, 0, "shell")
    _warp(user)
    result = store.harvest(user, 0)
    farm = store.get_farm(user["uid"])
    row = get_table("game_quests").find_one(farm_uid=farm["uid"], kind="earn")
    assert row["progress"] == result["coins"]


def test_claim_quest_success(local_db):
    user = _reset("unit_a")
    _insert_quest(user, "plant", goal=2, progress=2, reward_coins=70, reward_xp=8)
    before = _coins(user)
    result = store.claim_quest(user, "plant")
    assert result["reward_coins"] == 70
    assert _coins(user) == before + 70


def test_claim_quest_incomplete_blocked(local_db):
    user = _reset("unit_a")
    _insert_quest(user, "plant", goal=5, progress=1)
    with pytest.raises(GameError):
        store.claim_quest(user, "plant")


def test_claim_quest_double_blocked(local_db):
    user = _reset("unit_a")
    _insert_quest(user, "plant", goal=2, progress=2)
    store.claim_quest(user, "plant")
    with pytest.raises(GameError):
        store.claim_quest(user, "plant")


# --- fertilize ---------------------------------------------------------------


def test_fertilize_reduces_time_and_costs_coins(local_db):
    user = _reset("unit_a")
    store.plant(user, 0, "python")
    before_remaining = _remaining(user)
    before_coins = _coins(user)
    result = store.fertilize(user, 0)
    assert _remaining(user) < before_remaining
    assert _coins(user) == before_coins - result["spent"]


def test_fertilize_empty_plot_blocked(local_db):
    user = _reset("unit_a")
    with pytest.raises(GameError):
        store.fertilize(user, 0)


def test_fertilize_ready_build_blocked(local_db):
    user = _reset("unit_a")
    store.plant(user, 0, "shell")
    _warp(user)
    with pytest.raises(GameError):
        store.fertilize(user, 0)


# --- prestige ----------------------------------------------------------------


def test_prestige_below_level_blocked(local_db):
    user = _reset("unit_a")
    with pytest.raises(GameError):
        store.prestige(user)


def test_prestige_resets_farm_and_increments(local_db):
    user = _reset("unit_a")
    _set(
        user,
        xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL),
        perk_growth=3,
        ci_tier=3,
    )
    store.buy_plot(user)
    coins_before = _coins(user)
    fee = economy.refactor_cost(0, coins_before)
    carried = economy.refactor_carryover(coins_before, fee, 0)
    result = store.prestige(user)
    assert result["prestige"] == 1
    assert result["fee"] == fee
    assert result["carried"] == carried
    farm = store.get_farm(user["uid"])
    assert farm["coins"] == economy.STARTING_COINS + carried
    assert farm["xp"] == 0
    assert farm["ci_tier"] == 1
    assert farm["perk_growth"] == 0
    assert farm["plot_count"] == economy.STARTING_PLOTS
    assert len(store.get_plots(farm["uid"])) == economy.STARTING_PLOTS


def test_prestige_insufficient_coins_blocked(local_db):
    user = _reset("unit_a", coins=100)
    _set(user, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL))
    with pytest.raises(GameError):
        store.prestige(user)
    farm = store.get_farm(user["uid"])
    assert int(farm["prestige"] or 0) == 0
    assert int(farm["coins"]) == 100


def test_prestige_fee_funds_treasury(local_db):
    user = _reset("unit_a")
    _set(user, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL))
    fee = economy.refactor_cost(0, _coins(user))
    store.prestige(user)
    assert store.treasury_balance() == fee


def test_prestige_carryover_scales_with_legacy(local_db):
    user = _reset("unit_a")
    _set(
        user,
        xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL),
        legacy_carryover=5,
    )
    coins_before = _coins(user)
    fee = economy.refactor_cost(0, coins_before)
    carried = economy.refactor_carryover(coins_before, fee, 5)
    assert carried > economy.refactor_carryover(coins_before, fee, 0)
    store.prestige(user)
    farm = store.get_farm(user["uid"])
    assert farm["coins"] == economy.STARTING_COINS + carried


def test_prestige_fee_rises_with_prestige_and_wealth(local_db):
    assert economy.refactor_cost(1, 0) > economy.refactor_cost(0, 0)
    assert economy.refactor_cost(0, 1_000_000) > economy.refactor_cost(0, 0)


# --- legacy upgrades ---------------------------------------------------------


def test_upgrade_legacy_success_spends_stars(local_db):
    user = _reset("unit_a")
    _set(user, stars=100, legacy_multiplier=0)
    cost = economy.legacy_cost(economy.legacy_for("multiplier"), 0)
    result = store.upgrade_legacy(user, "multiplier")
    assert result["level"] == 1
    assert result["spent"] == cost
    farm = store.get_farm(user["uid"])
    assert int(farm["legacy_multiplier"]) == 1
    assert int(farm["stars"]) == 100 - cost


def test_upgrade_legacy_unknown_key(local_db):
    user = _reset("unit_a")
    _set(user, stars=100)
    with pytest.raises(GameError):
        store.upgrade_legacy(user, "telekinesis")


def test_upgrade_legacy_maxed(local_db):
    user = _reset("unit_a")
    up = economy.legacy_for("multiplier")
    _set(user, stars=100000, legacy_multiplier=up.max_level)
    with pytest.raises(GameError):
        store.upgrade_legacy(user, "multiplier")


def test_upgrade_legacy_insufficient_stars(local_db):
    user = _reset("unit_a")
    _set(user, stars=0, legacy_multiplier=0)
    with pytest.raises(GameError):
        store.upgrade_legacy(user, "multiplier")


# --- stealing ----------------------------------------------------------------


def _ripen_past_grace(user, slot=0):
    farm = store.get_farm(user["uid"])
    past = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
    plot = store._plot_at(farm["uid"], slot)
    get_table("game_plots").update(
        {"uid": plot["uid"], "planted_at": past, "ready_at": past}, ["uid"]
    )


def test_steal_transfers_a_share_and_leaves_the_build(local_db):
    owner = _reset("unit_owner")
    thief = _reset("unit_thief", coins=0)
    store.plant(owner, 0, "shell")
    _ripen_past_grace(owner)
    result = store.steal(thief, owner, 0)
    assert result["coins"] > 0
    assert _coins(thief) == result["coins"]
    farm = store.get_farm(owner["uid"])
    plot = store._plot_at(farm["uid"], 0)
    assert (plot.get("crop_key") or "") == "shell"
    assert 0 < float(plot.get("raided_fraction") or 0) <= 1.0


def test_owner_still_harvests_the_unraided_remainder(local_db):
    owner = _reset("unit_owner")
    thief = _reset("unit_thief", coins=0)
    store.plant(owner, 0, "shell")
    _ripen_past_grace(owner)
    before = _coins(owner)
    stolen = store.steal(thief, owner, 0)["coins"]
    result = store.harvest(owner, 0)
    harvested = result["coins"]
    assert harvested > 0
    assert _coins(owner) == before + harvested
    full = economy.realizable_harvest_coins(
        economy.crop_for("shell"), golden=result["golden"]
    )
    assert harvested + stolen <= full


def test_steal_blocked_once_the_build_is_fully_stripped(local_db):
    owner = _reset("unit_owner")
    store.plant(owner, 0, "shell")
    _ripen_past_grace(owner)
    farm = store.get_farm(owner["uid"])
    plot = store._plot_at(farm["uid"], 0)
    get_table("game_plots").update(
        {"uid": plot["uid"], "raided_fraction": 1.0}, ["uid"]
    )
    thief = _reset("unit_thief", coins=0)
    with pytest.raises(GameError):
        store.steal(thief, owner, 0)


def test_steal_capped_per_victim_per_day(local_db):
    owner = _reset("unit_owner")
    store.plant(owner, 0, "shell")
    _ripen_past_grace(owner)
    for index in range(economy.STEAL_MAX_PER_VICTIM_PER_DAY):
        thief = _reset(f"unit_thief_{index}", coins=0)
        store.steal(thief, owner, 0)
        farm = store.get_farm(owner["uid"])
        plot = store._plot_at(farm["uid"], 0)
        get_table("game_plots").update(
            {"uid": plot["uid"], "raided_fraction": 0.0}, ["uid"]
        )
    blocked = _reset("unit_thief_last", coins=0)
    with pytest.raises(GameError):
        store.steal(blocked, owner, 0)


def test_steal_protected_within_grace(local_db):
    owner = _reset("unit_owner")
    thief = _reset("unit_thief")
    store.plant(owner, 0, "shell")
    farm = store.get_farm(owner["uid"])
    past = (datetime.now(timezone.utc) - timedelta(seconds=5)).isoformat()
    plot = store._plot_at(farm["uid"], 0)
    get_table("game_plots").update(
        {"uid": plot["uid"], "planted_at": past, "ready_at": past}, ["uid"]
    )
    with pytest.raises(GameError):
        store.steal(thief, owner, 0)


def test_steal_cooldown_blocks_second_raid(local_db):
    owner = _reset("unit_owner")
    thief = _reset("unit_thief", coins=0)
    store.plant(owner, 0, "shell")
    _ripen_past_grace(owner)
    store.steal(thief, owner, 0)
    store.harvest(owner, 0)
    store.plant(owner, 0, "shell")
    _ripen_past_grace(owner)
    with pytest.raises(GameError):
        store.steal(thief, owner, 0)


# --- backwards compatibility -------------------------------------------------


def test_serialize_handles_legacy_row_without_new_columns(local_db):
    user = _user("unit_legacy")
    existing = store.get_farm(user["uid"])
    if existing:
        get_table("game_plots").delete(farm_uid=existing["uid"])
        get_table("game_quests").delete(farm_uid=existing["uid"])
        get_table("game_farms").delete(uid=existing["uid"])
    now = datetime.now(timezone.utc).isoformat()
    get_table("game_farms").insert(
        {
            "uid": "legacy-farm",
            "user_uid": user["uid"],
            "coins": 50,
            "xp": 0,
            "level": 1,
            "ci_tier": 1,
            "plot_count": economy.STARTING_PLOTS,
            "total_harvests": 0,
            "created_at": now,
            "updated_at": now,
        }
    )
    farm = get_table("game_farms").find_one(uid="legacy-farm")
    data = store.serialize_farm(farm, viewer=user, owner=user)
    assert data["prestige"] == 0
    assert data["streak"] == 0
    assert data["daily_available"] is True
    assert all(perk["level"] == 0 for perk in data["perks"])


# --- leaderboard -------------------------------------------------------------


def test_leaderboard_orders_by_score(local_db):
    high = _reset("unit_lb_high")
    low = _reset("unit_lb_low")
    _set(high, level=8, xp=economy.xp_threshold(8))
    _set(low, level=1, xp=0)
    board = store.leaderboard(50)
    names = [entry["username"] for entry in board]
    assert "unit_lb_high" in names and "unit_lb_low" in names
    assert names.index("unit_lb_high") < names.index("unit_lb_low")


def test_leaderboard_prestige_lifts_above_lower_level(local_db):
    refactorer = _reset("unit_lb_refactor")
    grinder = _reset("unit_lb_grind")
    _set(refactorer, level=1, xp=200, prestige=1)
    _set(grinder, level=5, xp=economy.xp_threshold(5))
    board = store.leaderboard(50)
    names = [entry["username"] for entry in board]
    assert names.index("unit_lb_refactor") < names.index("unit_lb_grind")


def test_leaderboard_entry_exposes_score_and_prestige(local_db):
    user = _reset("unit_lb_fields")
    _set(user, prestige=3, total_harvests=12, ci_tier=2)
    entry = next(e for e in store.leaderboard(50) if e["username"] == "unit_lb_fields")
    farm = store.get_farm(user["uid"])
    assert entry["prestige"] == 3
    assert entry["score"] == economy.farm_score(farm)
    assert {"rank", "username", "level", "xp", "coins", "total_harvests", "prestige", "score"} <= set(entry)


# --- market saturation ---------------------------------------------------------


def test_record_and_recent_harvests_roundtrip(local_db):
    _reset("unit_a")
    from devplacepy.services.game.store import market as market_store

    now = datetime.now(timezone.utc)
    market_store.record_harvest_tick("shell", now)
    market_store.record_harvest_tick("shell", now)
    assert store.recent_harvests("shell", economy.MARKET_WINDOW_HOURS) == 2
    assert store.recent_harvests("python", economy.MARKET_WINDOW_HOURS) == 0


def test_harvest_records_market_tick_and_saturates_reward(local_db):
    user = _reset("unit_a")
    from devplacepy.services.game.store import market as market_store
    from devplacepy.utils import generate_uid

    now = datetime.now(timezone.utc)
    crop = economy.crop_for("shell")
    get_table("game_market_ticks").insert(
        {
            "uid": generate_uid(),
            "crop_key": "shell",
            "hour_bucket": market_store._hour_bucket(now),
            "harvests": _worst_tier_harvests("shell"),
            "updated_at": now.isoformat(),
        }
    )
    market_store._saturation_cache.clear()
    store.plant(user, 0, "shell")
    _warp(user)
    result = store.harvest(user, 0)
    unsaturated = economy.realizable_harvest_coins(crop, golden=result["golden"])
    assert result["coins"] < unsaturated


def _seed_market_ticks(crop_key, harvests):
    from devplacepy.services.game.store import market as market_store
    from devplacepy.utils import generate_uid

    now = datetime.now(timezone.utc)
    get_table("game_market_ticks").insert(
        {
            "uid": generate_uid(),
            "crop_key": crop_key,
            "hour_bucket": market_store._hour_bucket(now),
            "harvests": harvests,
            "updated_at": now.isoformat(),
        }
    )
    market_store._saturation_cache.clear()


def _worst_tier_harvests(crop_key):
    from devplacepy.services.game.store import market as market_store

    crop = economy.crop_for(crop_key)
    worst_days = economy.MARKET_SATURATION_TIERS[-1][0]
    supply_per_harvest = crop.grow_seconds / 86400.0 / market_store.active_farms()
    return int(worst_days / supply_per_harvest) + 1


def test_starter_crop_boosted_when_market_saturated(local_db):
    _reset("unit_a")
    from devplacepy.services.game.store import market as market_store

    _seed_market_ticks("kernel", _worst_tier_harvests("kernel"))
    assert market_store.market_factor_for("kernel") == economy.MARKET_SATURATION_TIERS[-1][1]
    assert market_store.market_factor_for("shell") == economy.MARKET_BUFF_CAP
    assert market_store.market_factor_for("rust") == 1.0


def test_own_saturation_beats_boost(local_db):
    _reset("unit_a")
    from devplacepy.services.game.store import market as market_store

    _seed_market_ticks("kernel", _worst_tier_harvests("kernel"))
    _seed_market_ticks("shell", _worst_tier_harvests("shell"))
    assert market_store.market_factor_for("shell") == economy.MARKET_SATURATION_TIERS[-1][1]


def test_harvest_records_a_market_tick_for_its_own_crop(local_db):
    user = _reset("unit_a")
    store.plant(user, 0, "shell")
    _warp(user)
    store.harvest(user, 0)
    assert store.recent_harvests("shell", economy.MARKET_WINDOW_HOURS) == 1


# --- infrastructure --------------------------------------------------------------


def test_buy_infrastructure_success(local_db):
    cost = economy.infra_for("registry").cost
    user = _reset("unit_a", coins=cost + 100000)
    _set(user, prestige=5)
    result = store.buy_infrastructure(user, "registry")
    assert result["key"] == "registry"
    farm = store.get_farm(user["uid"])
    assert int(farm["infra_registry"]) == 1
    assert _coins(user) == 100000


def test_buy_infrastructure_requires_prestige(local_db):
    user = _reset("unit_a")
    with pytest.raises(GameError):
        store.buy_infrastructure(user, "registry")


def test_buy_infrastructure_already_owned_blocked(local_db):
    cost = economy.infra_for("registry").cost
    user = _reset("unit_a", coins=cost * 2)
    _set(user, prestige=5)
    store.buy_infrastructure(user, "registry")
    with pytest.raises(GameError):
        store.buy_infrastructure(user, "registry")


def test_buy_infrastructure_unknown_key(local_db):
    user = _reset("unit_a")
    with pytest.raises(GameError):
        store.buy_infrastructure(user, "does_not_exist")


# --- defense -----------------------------------------------------------------------


def test_upgrade_defense_success(local_db):
    user = _reset("unit_a")
    result = store.upgrade_defense(user)
    assert result["defense_level"] == 1
    assert int(store.get_farm(user["uid"])["defense_level"]) == 1


def test_upgrade_defense_insufficient_coins(local_db):
    user = _reset("unit_a", coins=0)
    with pytest.raises(GameError):
        store.upgrade_defense(user)


def test_charge_upkeep_decays_when_unaffordable(local_db):
    user = _reset("unit_a", coins=0)
    _set(user, defense_level=1, defense_last_upkeep_at=(datetime.now(timezone.utc) - timedelta(days=3)).isoformat())
    farm = store.get_farm(user["uid"])
    updated = store.charge_upkeep(farm, datetime.now(timezone.utc))
    assert int(updated["defense_level"]) == 0
    assert int(updated["coins"]) == 0


def test_charge_upkeep_deducts_coins_when_affordable(local_db):
    user = _reset("unit_a", coins=100000)
    _set(user, defense_level=1, defense_last_upkeep_at=(datetime.now(timezone.utc) - timedelta(days=1)).isoformat())
    farm = store.get_farm(user["uid"])
    updated = store.charge_upkeep(farm, datetime.now(timezone.utc))
    assert int(updated["defense_level"]) == 1
    assert int(updated["coins"]) < 100000


# --- cosmetics ---------------------------------------------------------------------


def test_buy_and_equip_cosmetic(local_db):
    user = _reset("unit_a", coins=economy.cosmetic_for("title_architect").cost_coins * 2)
    store.buy_cosmetic(user, "title_architect")
    assert "title_architect" in store.owned_cosmetic_keys(user["uid"])
    result = store.equip_title(user, "title_architect")
    assert result["active_title"] == "title_architect"
    assert store.get_farm(user["uid"])["active_title"] == "title_architect"


def test_buy_cosmetic_twice_blocked(local_db):
    user = _reset("unit_a", coins=economy.cosmetic_for("title_architect").cost_coins * 2)
    store.buy_cosmetic(user, "title_architect")
    with pytest.raises(GameError):
        store.buy_cosmetic(user, "title_architect")


def test_equip_unowned_cosmetic_blocked(local_db):
    user = _reset("unit_a")
    with pytest.raises(GameError):
        store.equip_title(user, "title_architect")


def test_equip_non_title_cosmetic_blocked(local_db):
    user = _reset("unit_a", coins=economy.cosmetic_for("skin_neon").cost_coins * 2)
    store.buy_cosmetic(user, "skin_neon")
    with pytest.raises(GameError):
        store.equip_title(user, "skin_neon")


# --- mastery -------------------------------------------------------------------------


def test_upgrade_mastery_success(local_db):
    user = _reset("unit_a")
    _set(user, mastery_points=10)
    result = store.upgrade_mastery(user, "autoreplant")
    assert result["level"] == 1
    farm = store.get_farm(user["uid"])
    assert int(farm["mastery_autoreplant"]) == 1
    assert int(farm["mastery_points"]) == 10 - economy.mastery_cost(economy.mastery_for("autoreplant"), 0)


def test_upgrade_mastery_insufficient_points(local_db):
    user = _reset("unit_a")
    with pytest.raises(GameError):
        store.upgrade_mastery(user, "autoreplant")


def test_upgrade_mastery_unknown_key(local_db):
    user = _reset("unit_a")
    _set(user, mastery_points=1000)
    with pytest.raises(GameError):
        store.upgrade_mastery(user, "does_not_exist")


def test_farm_analytics_hidden_until_unlocked(local_db):
    user = _reset("unit_a")
    state = store.serialize_farm(store.get_farm(user["uid"]), viewer=user, owner=user)
    assert state["mastery_analytics_unlocked"] is False


def test_farm_analytics_tracks_lifetime_totals_once_unlocked(local_db):
    user = _reset("unit_a")
    _set(user, mastery_points=1000)
    store.upgrade_mastery(user, "analytics")
    store.plant(user, 0, "shell")
    _warp(user)
    result = store.harvest(user, 0)
    state = store.serialize_farm(store.get_farm(user["uid"]), viewer=user, owner=user)
    assert state["mastery_analytics_unlocked"] is True
    assert state["lifetime_coins_earned"] == result["coins"]
    assert state["lifetime_harvests"] == 1


def test_prestige_awards_mastery_at_threshold(local_db):
    user = _reset("unit_a")
    coins = economy.refactor_cost(49, 0) * 2
    _set(user, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL), prestige=49, coins=coins)
    result = store.prestige(user)
    assert result["mastery_awarded"] == 1
    farm = store.get_farm(user["uid"])
    assert int(farm["mastery_points"]) == 1
    assert int(farm["mastery_points_earned_total"]) == 1


def test_prestige_below_mastery_threshold_awards_nothing(local_db):
    user = _reset("unit_a")
    _set(user, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL), prestige=0)
    result = store.prestige(user)
    assert result["mastery_awarded"] == 0


# --- eras --------------------------------------------------------------------------


def test_start_era_resets_visible_counters_not_real_balances(local_db):
    user = _reset("unit_a", coins=12345)
    _set(user, era_coins=999, era_harvests=5, prestige=3, stars=7)
    if store.active_era():
        store.end_era()
    store.start_era("TestEra", 7)
    farm = store.get_farm(user["uid"])
    assert int(farm["era_coins"]) == 0
    assert int(farm["era_harvests"]) == 0
    assert int(farm["coins"]) == 12345
    assert int(farm["prestige"]) == 3
    assert int(farm["stars"]) == 7
    store.end_era()


def test_start_era_twice_blocked(local_db):
    if store.active_era():
        store.end_era()
    store.start_era("TestEra", 7)
    with pytest.raises(GameError):
        store.start_era("AnotherEra", 7)
    store.end_era()


def test_end_era_without_active_blocked(local_db):
    if store.active_era():
        store.end_era()
    with pytest.raises(GameError):
        store.end_era()


def test_end_era_awards_stars_to_top_participant(local_db):
    if store.active_era():
        store.end_era()
    user = _reset("unit_era_top")
    store.start_era("TestEra", 7)
    _set(user, era_coins=50000, era_harvests=10)
    before_stars = int(store.get_farm(user["uid"])["stars"] or 0)
    store.end_era()
    farm = store.get_farm(user["uid"])
    assert int(farm["stars"] or 0) > before_stars
    assert int(farm["era_coins"] or 0) == 0


def test_era_gates_new_crops_and_leaderboard(local_db):
    if store.active_era():
        store.end_era()
    _clear_game_caches()
    assert store.leaderboard_for("era") == []


# --- weekly contracts ------------------------------------------------------------------


def test_weekly_contract_requires_mastery_upgrade(local_db):
    user = _reset("unit_a")
    with pytest.raises(GameError):
        store.claim_quest(user, "harvest", "weekly")


def test_weekly_contract_claim_pays_stars_and_boost(local_db):
    user = _reset("unit_a")
    _set(user, mastery_points=1000)
    store.upgrade_mastery(user, "contracts")
    farm = store.get_farm(user["uid"])
    from devplacepy.services.game.store.quests import ensure_weekly_contract
    from devplacepy.services.game.store.common import _iso_week

    iso_week = _iso_week(datetime.now(timezone.utc))
    ensure_weekly_contract(farm, iso_week)
    row = get_table("game_quests").find_one(farm_uid=farm["uid"], day=iso_week, scope="weekly")
    get_table("game_quests").update({"uid": row["uid"], "progress": row["goal"]}, ["uid"])
    result = store.claim_quest(user, row["kind"], "weekly")
    assert result["reward_stars"] > 0
    farm = store.get_farm(user["uid"])
    assert int(farm["stars"]) == result["reward_stars"]
    assert farm.get("contract_boost_until")


# --- treasury and community grant --------------------------------------------


def _seed_treasury(amount):
    store.ensure_treasury()
    get_table("game_treasury").update(
        {"uid": "treasury-main", "balance": amount}, ["uid"]
    )


def _make_grant_eligible(user):
    _set(
        user,
        coins=100,
        harvests_week=economy.GRANT_MIN_WEEK_HARVESTS,
        harvests_week_start=store._iso_week(store._now()),
    )


def test_claim_grant_pays_from_treasury(local_db):
    user = _reset("unit_a")
    _seed_treasury(100_000)
    _make_grant_eligible(user)
    result = store.claim_grant(user)
    assert result["amount"] == economy.GRANT_CAP
    assert _coins(user) == 100 + economy.GRANT_CAP
    assert store.treasury_balance() == 100_000 - economy.GRANT_CAP


def test_claim_grant_once_per_week(local_db):
    user = _reset("unit_a")
    _seed_treasury(100_000)
    _make_grant_eligible(user)
    store.claim_grant(user)
    _set(user, coins=100)
    with pytest.raises(GameError):
        store.claim_grant(user)


def test_claim_grant_requires_activity(local_db):
    user = _reset("unit_a")
    _seed_treasury(100_000)
    _set(user, coins=100, harvests_week=economy.GRANT_MIN_WEEK_HARVESTS - 1)
    with pytest.raises(GameError):
        store.claim_grant(user)


def test_claim_grant_wealth_ceiling(local_db):
    user = _reset("unit_a")
    _seed_treasury(100_000)
    _set(
        user,
        coins=economy.GRANT_WEALTH_CEILING,
        harvests_week=economy.GRANT_MIN_WEEK_HARVESTS,
    )
    with pytest.raises(GameError):
        store.claim_grant(user)


def test_claim_grant_prestige_cap(local_db):
    user = _reset("unit_a")
    _seed_treasury(100_000)
    _make_grant_eligible(user)
    _set(user, prestige=economy.GRANT_MAX_PRESTIGE + 1)
    with pytest.raises(GameError):
        store.claim_grant(user)


def test_claim_grant_empty_treasury(local_db):
    user = _reset("unit_a")
    _make_grant_eligible(user)
    with pytest.raises(GameError):
        store.claim_grant(user)


def test_claim_grant_partial_treasury(local_db):
    user = _reset("unit_a")
    _seed_treasury(300)
    _make_grant_eligible(user)
    result = store.claim_grant(user)
    assert result["amount"] == 300
    assert store.treasury_balance() == 0
