"""Tests for core engine mechanics: containment, flags, clock, articles."""

from h2g2.engine.game_object import GameObject, Room, Flag
from h2g2.engine.clock import Clock


class TestObjectContainment:
    def test_move_to_sets_parent(self):
        room = Room("R1", desc="room")
        obj = GameObject("O1", desc="thing")
        obj.move_to(room)
        assert obj.parent is room
        assert obj in room.children

    def test_move_to_removes_from_old_parent(self):
        room1 = Room("R1", desc="room1")
        room2 = Room("R2", desc="room2")
        obj = GameObject("O1", desc="thing")
        obj.move_to(room1)
        obj.move_to(room2)
        assert obj not in room1.children
        assert obj in room2.children
        assert obj.parent is room2

    def test_contains(self):
        room = Room("R1", desc="room")
        obj = GameObject("O1", desc="thing")
        obj.move_to(room)
        assert room.contains(obj)

    def test_is_held_by_direct(self):
        holder = GameObject("H", desc="holder")
        obj = GameObject("O", desc="thing")
        obj.move_to(holder)
        assert obj.is_held_by(holder)

    def test_is_held_by_nested(self):
        holder = GameObject("H", desc="holder")
        container = GameObject("C", desc="container")
        obj = GameObject("O", desc="thing")
        container.move_to(holder)
        obj.move_to(container)
        assert obj.is_held_by(holder)

    def test_is_held_by_false(self):
        holder = GameObject("H", desc="holder")
        obj = GameObject("O", desc="thing")
        assert not obj.is_held_by(holder)

    def test_contents_string_excludes_invisible(self):
        room = Room("R1", desc="room")
        visible = GameObject("V", desc="visible")
        invisible = GameObject("I", desc="invisible", flags={Flag.INVISIBLE})
        visible.move_to(room)
        invisible.move_to(room)
        result = room.contents_string()
        assert visible in result
        assert invisible not in result

    def test_contents_string_excludes_ndescbit(self):
        room = Room("R1", desc="room")
        visible = GameObject("V", desc="visible")
        hidden = GameObject("H", desc="hidden", flags={Flag.NDESCBIT})
        visible.move_to(room)
        hidden.move_to(room)
        result = room.contents_string()
        assert visible in result
        assert hidden not in result


class TestFlagOperations:
    def test_fset_and_fset_q(self):
        obj = GameObject("O", desc="thing")
        assert not obj.fset_q(Flag.TAKEBIT)
        obj.fset(Flag.TAKEBIT)
        assert obj.fset_q(Flag.TAKEBIT)

    def test_fclear(self):
        obj = GameObject("O", desc="thing", flags={Flag.TAKEBIT})
        assert obj.fset_q(Flag.TAKEBIT)
        obj.fclear(Flag.TAKEBIT)
        assert not obj.fset_q(Flag.TAKEBIT)

    def test_fclear_nonexistent_flag_no_error(self):
        obj = GameObject("O", desc="thing")
        obj.fclear(Flag.TAKEBIT)  # should not raise

    def test_multiple_flags(self):
        obj = GameObject("O", desc="thing", flags={Flag.TAKEBIT, Flag.OPENBIT})
        assert obj.fset_q(Flag.TAKEBIT)
        assert obj.fset_q(Flag.OPENBIT)
        assert not obj.fset_q(Flag.WEARBIT)


class TestObjectArticle:
    def test_default_article(self):
        obj = GameObject("O", desc="ball")
        assert obj.article() == "a "
        assert obj.a_desc() == "a ball"
        assert obj.the_desc() == "the ball"

    def test_vowel_article(self):
        obj = GameObject("O", desc="egg", flags={Flag.VOWELBIT})
        assert obj.article() == "an "
        assert obj.a_desc() == "an egg"

    def test_no_article(self):
        obj = GameObject("O", desc="Ford Prefect", flags={Flag.NARTICLEBIT})
        assert obj.article() == ""
        assert obj.a_desc() == "Ford Prefect"
        assert obj.the_desc() == "Ford Prefect"


class TestClock:
    def test_tick_and_fire(self):
        clock = Clock()
        fired = []

        def handler(state):
            fired.append(True)
            return True

        clock.queue(handler, 3, name="test")
        clock.tick_all(None)  # tick 1: countdown 3->2
        assert len(fired) == 0
        clock.tick_all(None)  # tick 2: countdown 2->1
        assert len(fired) == 0
        clock.tick_all(None)  # tick 3: countdown 1->0, fires
        assert len(fired) == 1

    def test_one_shot_does_not_refire(self):
        clock = Clock()
        fired = []

        def handler(state):
            fired.append(True)
            return True

        clock.queue(handler, 1, name="test")
        clock.tick_all(None)  # fires
        clock.tick_all(None)  # should not fire again
        assert len(fired) == 1

    def test_disable_prevents_fire(self):
        clock = Clock()
        fired = []

        def handler(state):
            fired.append(True)
            return True

        entry = clock.queue(handler, 2, name="test")
        clock.tick_all(None)  # tick 1
        clock.disable(entry)
        clock.tick_all(None)  # tick 2 — disabled, should not fire
        clock.tick_all(None)  # tick 3
        assert len(fired) == 0

    def test_negative_tick_fires_every_turn(self):
        clock = Clock()
        fired = []

        def handler(state):
            fired.append(True)
            return True

        clock.queue(handler, -1, name="repeating")
        clock.tick_all(None)
        clock.tick_all(None)
        clock.tick_all(None)
        assert len(fired) == 3

    def test_queue_updates_existing_entry(self):
        clock = Clock()
        fired = []

        def handler(state):
            fired.append(True)
            return True

        clock.queue(handler, 5, name="test")
        clock.queue(handler, 1, name="test")  # reset to 1
        clock.tick_all(None)  # should fire now (1->0)
        assert len(fired) == 1

    def test_remove_entry(self):
        clock = Clock()
        fired = []

        def handler(state):
            fired.append(True)
            return True

        entry = clock.queue(handler, 1, name="test")
        clock.remove(entry)
        clock.tick_all(None)
        assert len(fired) == 0
