import datetime

import pytest
from sqlalchemy.exc import IntegrityError, NoResultFound

from idlemaxx.game import ActivityReward, Game
from idlemaxx.models import (
    CharacterItem,
    Skill,
    now_utc,
    Activity,
    Item,
)


@pytest.fixture(scope="function")
def game() -> Game:
    return Game("sqlite:///:memory:")


def test_create_character(game: Game) -> None:
    character = game.create_character("Foo")
    assert character.name == "Foo"
    assert len(character.skills) == len(Skill)


def test_get_character_by_name(game: Game) -> None:
    character = game.create_character("Foo")
    assert character == game.get_character_by_name("Foo")

    with pytest.raises(NoResultFound):
        game.get_character_by_name("Bar")


def test_start_activity(game: Game) -> None:
    character = game.create_character("Foo")
    character_activity = game.start_activity(character, Activity.CHOP_REGULAR_TREE)

    assert character_activity.started_at is not None
    assert character_activity.ended_at is None


def test_end_activity(game: Game) -> None:
    character = game.create_character("Foo")
    character_activity = game.start_activity(character, Activity.CHOP_REGULAR_TREE)
    rewards = game.end_activity(character)

    assert isinstance(rewards, list)
    assert len(rewards) == 1
    assert isinstance(rewards[0], ActivityReward)
    assert character_activity.ended_at is not None

    # No active activity
    assert game.end_activity(character) == []


def test_only_one_activity(game: Game) -> None:
    character = game.create_character("Foo")
    game.start_activity(character, Activity.CHOP_REGULAR_TREE)

    with pytest.raises(Exception):
        game.start_activity(character, Activity.MINE_TIN)


def test_switch_activity(game: Game) -> None:
    character = game.create_character("Foo")
    character_activity = game.start_activity(character, Activity.CHOP_REGULAR_TREE)
    game.end_activity(character)
    character_activity_2 = game.start_activity(character, Activity.MINE_TIN)

    assert character_activity.ended_at is not None
    assert character_activity_2.started_at >= character_activity.ended_at
    assert character_activity_2.ended_at is None


def test_character_activity_skill_requirements(game: Game) -> None:
    character = game.create_character("Foo")

    # level 1 woodcutting is sufficient
    game.start_activity(character, Activity.CHOP_REGULAR_TREE)

    # level 80 woodcutting required — should raise
    game.end_activity(character)
    with pytest.raises(Exception):
        game.start_activity(character, Activity.CHOP_REDWOOD)


def test_character_activity_skill_rewards(game: Game) -> None:
    character = game.create_character("Foo")
    woodcutting = next(s for s in character.skills if s.skill == Skill.WOODCUTTING)
    assert woodcutting.experience == 0

    character_activity = game.start_activity(character, Activity.CHOP_REGULAR_TREE)
    character_activity.started_at = now_utc() - datetime.timedelta(seconds=10)
    game.end_activity(character)

    # 10s / 0.5s action_time = 20 actions, 1 xp each
    assert woodcutting.experience == 20

    character_activity = game.start_activity(character, Activity.CHOP_REGULAR_TREE)
    character_activity.started_at = now_utc() - datetime.timedelta(seconds=15)
    game.end_activity(character)

    # 30 more actions → 50 total
    assert woodcutting.experience == 50


def test_character_activity_item_rewards(game: Game) -> None:
    character = game.create_character("Foo")

    character_activity = game.start_activity(character, Activity.CHOP_REGULAR_TREE)
    character_activity.started_at = now_utc() - datetime.timedelta(seconds=10)
    game.end_activity(character)

    logs = next(i for i in character.items if i.item == Item.REGULAR_LOG)
    assert logs.quantity == 20

    character_activity = game.start_activity(character, Activity.CHOP_REGULAR_TREE)
    character_activity.started_at = now_utc() - datetime.timedelta(seconds=15)
    game.end_activity(character)

    assert logs.quantity == 50


def test_character_skill_non_negative_xp(game: Game) -> None:
    character = game.create_character("Foo")
    woodcutting = next(s for s in character.skills if s.skill == Skill.WOODCUTTING)
    assert woodcutting.experience == 0

    woodcutting.experience = -50
    with pytest.raises(IntegrityError):
        game._session.commit()


def test_character_item_non_negative_quantity(game: Game) -> None:
    character = game.create_character("Foo")
    character_activity = game.start_activity(character, Activity.CHOP_REGULAR_TREE)
    character_activity.started_at = now_utc() - datetime.timedelta(seconds=10)
    game.end_activity(character)

    logs = next(i for i in character.items if i.item == Item.REGULAR_LOG)
    assert logs.quantity == 20

    logs.quantity = -50
    with pytest.raises(IntegrityError):
        game._session.commit()


def test_activity_materials_consumed(game: Game) -> None:
    character = game.create_character("Foo")
    character.items.append(CharacterItem(character_id=character.id, item=Item.COPPER_ORE, quantity=1))
    character.items.append(CharacterItem(character_id=character.id, item=Item.TIN_ORE, quantity=1))

    character_activity = game.start_activity(character, Activity.SMELT_COPPER_BAR)
    character_activity.started_at = now_utc() - datetime.timedelta(seconds=Activity.SMELT_COPPER_BAR.action_time * 2)

    rewards = game.end_activity(character)

    assert len(rewards) == 1
    reward = rewards[0]
    assert reward.actions_completed == 1
    assert reward.items_gained == {Item.COPPER_BAR: 1}
    assert reward.materials_consumed == {Item.COPPER_ORE: 1, Item.TIN_ORE: 1}

    copper_bars = next(i for i in character.items if i.item == Item.COPPER_BAR)
    copper_ore = next(i for i in character.items if i.item == Item.COPPER_ORE)
    tin_ore = next(i for i in character.items if i.item == Item.TIN_ORE)

    assert copper_bars.quantity == 1
    assert copper_ore.quantity == 0
    assert tin_ore.quantity == 0


def test_action_limit_caps_actions(game: Game) -> None:
    character = game.create_character("Foo")
    ca = game.start_activity(character, Activity.CHOP_REGULAR_TREE, action_limit=5)
    ca.started_at = now_utc() - datetime.timedelta(seconds=100)  # way more time than needed
    rewards = game.end_activity(character)
    assert rewards[0].actions_completed == 5


def test_action_limit_material_auto_cap(game: Game) -> None:
    """Material limit caps actions even when action_limit is higher."""
    character = game.create_character("Foo")
    character.items.append(CharacterItem(character_id=character.id, item=Item.COPPER_ORE, quantity=2))
    character.items.append(CharacterItem(character_id=character.id, item=Item.TIN_ORE, quantity=2))
    ca = game.start_activity(character, Activity.SMELT_COPPER_BAR, action_limit=100)
    ca.started_at = now_utc() - datetime.timedelta(seconds=Activity.SMELT_COPPER_BAR.action_time * 50)
    rewards = game.end_activity(character)
    assert rewards[0].actions_completed == 2  # limited by materials


def test_queue_activity_starts_immediately_if_idle(game: Game) -> None:
    character = game.create_character("Foo")
    game.queue_activity(character, Activity.CHOP_REGULAR_TREE)
    assert character.current_activity is not None
    assert character.current_activity.activity == Activity.CHOP_REGULAR_TREE


def test_queue_activity_adds_to_queue_if_busy(game: Game) -> None:
    character = game.create_character("Foo")
    game.start_activity(character, Activity.CHOP_REGULAR_TREE, action_limit=10)
    game.queue_activity(character, Activity.MINE_TIN)
    queue = game.get_activity_queue(character)
    assert len(queue) == 1
    assert queue[0].activity == Activity.MINE_TIN


def test_queue_limit_enforced(game: Game) -> None:
    character = game.create_character("Foo")
    game.start_activity(character, Activity.CHOP_REGULAR_TREE, action_limit=10)
    game.queue_activity(character, Activity.MINE_TIN)
    game.queue_activity(character, Activity.MINE_COPPER)
    game.queue_activity(character, Activity.MINE_IRON)
    with pytest.raises(Exception):
        game.queue_activity(character, Activity.MINE_TIN)


def test_end_activity_chains_queue(game: Game) -> None:
    """When activity A hits its action limit before now, queued activity B starts retroactively."""
    character = game.create_character("Foo")
    # MINE_TIN: 3 actions at 1s each → done after 3s from start
    ca = game.start_activity(character, Activity.MINE_TIN, action_limit=3)
    ca.started_at = now_utc() - datetime.timedelta(seconds=10)
    # Queue MINE_IRON: 2 actions at 1.5s each
    game.queue_activity(character, Activity.MINE_IRON, action_limit=2)

    rewards = game.end_activity(character)
    assert len(rewards) == 2
    assert rewards[0].actions_completed == 3  # MINE_TIN completed its limit
    assert rewards[1].actions_completed == 2  # MINE_IRON completed its limit


def test_end_activity_retroactive_multiple(game: Game) -> None:
    """Three activities chain: A (limit 2) → B (limit 3) → C (no limit, still running)."""
    character = game.create_character("Foo")
    # CHOP_REGULAR_TREE: 0.5s per action, limit 2 → done after 1s
    ca = game.start_activity(character, Activity.CHOP_REGULAR_TREE, action_limit=2)
    ca.started_at = now_utc() - datetime.timedelta(seconds=20)

    game.queue_activity(character, Activity.MINE_TIN, action_limit=3)  # 1s each → done after 3s from chain
    game.queue_activity(character, Activity.MINE_COPPER)  # no limit → still running

    rewards = game.end_activity(character)
    # CHOP completes at +1s, MINE_TIN at +4s, MINE_COPPER runs from +4s to now (~16s)
    assert len(rewards) == 3
    assert rewards[0].actions_completed == 2  # CHOP_REGULAR_TREE
    assert rewards[1].actions_completed == 3  # MINE_TIN
    assert rewards[2].actions_completed >= 1  # MINE_COPPER (16s / 1s = 16 actions)


def test_get_activity_queue_returns_pending_only(game: Game) -> None:
    character = game.create_character("Foo")
    game.start_activity(character, Activity.CHOP_REGULAR_TREE, action_limit=1)
    game.queue_activity(character, Activity.MINE_TIN)
    game.queue_activity(character, Activity.MINE_COPPER)

    queue = game.get_activity_queue(character)
    assert len(queue) == 2

    # Simulate enough time for CHOP to complete and chain into MINE_TIN
    ca = character.current_activity
    assert ca is not None
    ca.started_at = now_utc() - datetime.timedelta(seconds=10)
    game.end_activity(character)

    # MINE_TIN was dequeued (started_at set); MINE_COPPER may still be pending or also dequeued
    queue_after = game.get_activity_queue(character)
    assert len(queue_after) <= 1


def test_remove_from_queue(game: Game) -> None:
    character = game.create_character("Foo")
    game.start_activity(character, Activity.CHOP_REGULAR_TREE, action_limit=10)
    game.queue_activity(character, Activity.MINE_TIN)
    game.queue_activity(character, Activity.MINE_COPPER)

    queue = game.get_activity_queue(character)
    assert len(queue) == 2

    # Remove the first item
    game.remove_from_queue(character, queue[0])

    queue_after = game.get_activity_queue(character)
    assert len(queue_after) == 1
    assert queue_after[0].activity == Activity.MINE_COPPER


def test_end_unlimited_activity_starts_next_queued(game: Game) -> None:
    """Ending an unlimited activity should automatically start the next queued activity."""
    character = game.create_character("Foo")
    game.start_activity(character, Activity.CHOP_REGULAR_TREE)
    game.queue_activity(character, Activity.MINE_TIN)
    game.queue_activity(character, Activity.MINE_COPPER)

    game.end_activity(character)

    assert character.current_activity is not None
    assert character.current_activity.activity == Activity.MINE_TIN
    assert len(game.get_activity_queue(character)) == 1
    assert game.get_activity_queue(character)[0].activity == Activity.MINE_COPPER


def test_queue_activity_checks_skill_requirements(game: Game) -> None:
    """Queuing an activity the character doesn't meet requirements for should raise."""
    character = game.create_character("Foo")
    game.start_activity(character, Activity.CHOP_REGULAR_TREE)

    with pytest.raises(Exception, match="level"):
        game.queue_activity(character, Activity.CHOP_REDWOOD)


def test_remove_from_queue_middle(game: Game) -> None:
    character = game.create_character("Foo")
    game.start_activity(character, Activity.CHOP_REGULAR_TREE, action_limit=10)
    game.queue_activity(character, Activity.MINE_TIN)
    game.queue_activity(character, Activity.MINE_COPPER)
    game.queue_activity(character, Activity.MINE_IRON)

    queue = game.get_activity_queue(character)
    assert len(queue) == 3

    # Remove the middle item
    game.remove_from_queue(character, queue[1])

    queue_after = game.get_activity_queue(character)
    assert len(queue_after) == 2
    assert queue_after[0].activity == Activity.MINE_TIN
    assert queue_after[1].activity == Activity.MINE_IRON
