"""Testy in-process botov (api/bots.py + tahova slucka v api/__init__.py).

Rovnaky setup ako tests/test_history.py: docasny SQLite subor, env pred
importom. Socket.IO emity idu do prazdnych roomov (ziadny klient), takze
handlery a slucka sa daju volat priamo bez klienta.
"""

import asyncio
import os
import tempfile
import unittest
import uuid

# Nastav DB/ENCRYPTION_KEY PRED importom db/api modulov.
_DB_FILE = os.path.join(tempfile.gettempdir(), f"bridzik_test_{uuid.uuid4().hex}.db")
os.environ["DATABASE_URL"] = "sqlite+aiosqlite:///" + _DB_FILE.replace("\\", "/")

from cryptography.fernet import Fernet  # noqa: E402

os.environ.setdefault("ENCRYPTION_KEY", Fernet.generate_key().decode())

from random import Random  # noqa: E402

import api  # noqa: E402
from api import auth, bots, history  # noqa: E402
from db.db import init_db  # noqa: E402
from rl.players import HeuristicPlayer, RandomPlayer  # noqa: E402


def run(coro):
    return asyncio.run(coro)


def _make_bot_accounts(n):
    exclude = set()
    accounts = []
    for _ in range(n):
        acc = run(bots.ensure_bot_account("random", exclude))
        exclude.add(acc["player_id"])
        accounts.append(acc)
    return accounts


def _make_game(seat_accounts, brains):
    """Postavi zacatu in-memory hru + Game riadok v DB."""
    gid = str(uuid.uuid4())
    game = api.Game(gid, "test")
    for seat, acc in enumerate(seat_accounts):
        player = api.Player(None, acc["username"], seat, acc["player_id"])
        if brains[seat] is not None:
            player.is_bot = True
            player.brain = brains[seat]
            player.connected = True
        else:
            player.connected = False
        game.players.append(player)
    game.start()
    api.games[gid] = game
    run(history.record_game_started(
        gid, "test", [acc["player_id"] for acc in seat_accounts]
    ))
    return game


class BotAccountCase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        run(init_db())
        api.BOT_MOVE_DELAY_SECONDS = 0
        api.TRICK_SWEEP_SECONDS = 0

    def test_username_conventions(self):
        self.assertTrue(bots.is_bot_username("bot:heuristic-1"))
        self.assertFalse(bots.is_bot_username("alice"))
        self.assertEqual(bots.kind_of("bot:heuristic-2"), "heuristic")
        self.assertEqual(bots.kind_of("bot:random-1"), "random")
        self.assertEqual(bots.kind_of("bot:neural-1"), "neural")
        self.assertEqual(bots.kind_of("bot:nezmysel-9"), bots.DEFAULT_KIND)
        self.assertIsInstance(bots.make_brain("bot:heuristic-1"), HeuristicPlayer)
        self.assertIsInstance(bots.make_brain("bot:random-3"), RandomPlayer)

    @unittest.skipUnless(bots.neural_available(),
                         'chyba export vah (py -m rl.export)')
    def test_neural_kind(self):
        from rl.pure_net import PureNeuralPlayer
        self.assertIn("neural", bots.available_kinds())
        brain = bots.make_brain("bot:neural-1")
        self.assertIsInstance(brain, PureNeuralPlayer)
        # zdielana instancia PureNet (vahy sa nacitavaju len raz)
        self.assertIs(brain.net, bots.make_brain("bot:neural-2").net)

    def test_ensure_bot_account_reuse_and_exclude(self):
        first = run(bots.ensure_bot_account("heuristic", set()))
        self.assertTrue(first["username"].startswith("bot:heuristic-"))
        # bez vylucenia sa ucet recykluje
        again = run(bots.ensure_bot_account("heuristic", set()))
        self.assertEqual(first["player_id"], again["player_id"])
        # s vylucenim vznikne dalsi ucet s inym ID
        second = run(bots.ensure_bot_account("heuristic", {first["player_id"]}))
        self.assertNotEqual(first["player_id"], second["player_id"])
        self.assertNotEqual(first["username"], second["username"])

    def test_bot_account_cannot_be_hijacked(self):
        acc = run(bots.ensure_bot_account("heuristic", set()))
        # registracia mena zlyha -- ucet sa netvari ako nedokoncena registracia
        with self.assertRaises(auth.AuthError):
            run(auth.register_account(acc["username"]))
        # login zlyha na kode (secret nikto nepozna), NIE na RegistrationIncomplete
        with self.assertRaises(auth.AuthError) as ctx:
            run(auth.login(acc["username"], "000000"))
        self.assertNotIsInstance(ctx.exception, auth.RegistrationIncomplete)


class BotTurnLoopCase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        run(init_db())
        api.BOT_MOVE_DELAY_SECONDS = 0
        api.TRICK_SWEEP_SECONDS = 0

    def setUp(self):
        api.games.clear()
        api.sessions.clear()
        api.accounts.clear()

    def test_four_bots_play_whole_game(self):
        accounts = _make_bot_accounts(4)
        brains = [RandomPlayer(Random(seat)) for seat in range(4)]
        game = _make_game(accounts, brains)

        run(api._run_bot_turns(game.gid))

        self.assertTrue(game.bridzik_core.is_completed())
        # cela hra je zapisana: 4 serie x 8 kol, ended_at nastaveny
        detail = run(history.get_game_detail(game.gid))
        self.assertIsNotNone(detail["ended_at"])
        self.assertEqual(len(detail["rounds"]), history.FULL_GAME_ROUNDS * 4)

    def test_bots_stop_at_human_turn(self):
        accounts = _make_bot_accounts(3)
        # sedadlo 0 = clovek; identitu v DB mu robi dalsi (nepouzity) boti
        # ucet -- pre historiu je to len player_id, wrapper bez mozgu = clovek
        human_acc = run(bots.ensure_bot_account(
            "random", {a["player_id"] for a in accounts}
        ))
        game = _make_game(
            [human_acc] + accounts,
            [None, HeuristicPlayer(Random(1), n_samples=20),
             RandomPlayer(Random(2)), RandomPlayer(Random(3))],
        )
        core = game.bridzik_core
        rnd = core.series[-1].get_last_round()

        async def scenario():
            # na tahu je clovek (first_player serie 0 je sedadlo 0) -> boti nic
            await api._run_bot_turns(game.gid)
            self.assertEqual(len(rnd.guesses), 0)
            # clovek tipne -> boti dotipuju a hraju az po dalsi tah cloveka
            core.add_player_guess(0, 1)
            await api._run_bot_turns(game.gid)

        run(scenario())
        self.assertTrue(rnd.is_guessing_completed())
        self.assertEqual(rnd.get_active_player(), 0)

    def test_add_and_remove_bot_handlers(self):
        async def scenario():
            gid = str(uuid.uuid4())
            api.games[gid] = api.Game(gid, "lobby-test")
            host = api.Player("sid-host", "hostiteľ", 0, 999_100)
            api.games[gid].players.append(host)
            api.sessions["sid-host"] = {"gid": gid, "order": 0}
            api.sessions["sid-guest"] = {"gid": gid, "order": 1}

            # nehostitel nesmie pridat bota
            await api.add_bot("sid-guest", gid)
            self.assertEqual(len(api.games[gid].players), 1)

            # hostitel prida dvoch botov -> rozne ucty, najnizsie volne sedadla
            await api.add_bot("sid-host", gid)
            await api.add_bot("sid-host", gid, "random")
            players = api.games[gid].players
            self.assertEqual(len(players), 3)
            bots_added = [p for p in players if p.is_bot]
            self.assertEqual(len(bots_added), 2)
            self.assertEqual({p.order for p in bots_added}, {1, 2})
            self.assertNotEqual(bots_added[0].player_id, bots_added[1].player_id)
            self.assertIsNotNone(bots_added[0].brain)

            # remove_bot: odmietne cloveka, odoberie bota
            await api.remove_bot("sid-host", gid, 0)
            self.assertEqual(len(api.games[gid].players), 3)
            await api.remove_bot("sid-host", gid, 1)
            self.assertEqual(len(api.games[gid].players), 2)
            self.assertIsNone(api.games[gid].player_by_order(1))

        run(scenario())

    def test_restore_marks_bots(self):
        from bridzik import Bridzik
        info = {
            "gid": str(uuid.uuid4()),
            "name": "obnova",
            "seats": [(1, "alice"), (2, "bot:heuristic-1"),
                      (3, "bot:random-1"), (4, "bob")],
            "core": Bridzik(),
        }
        game = api._load_game_into_memory(info)
        self.assertFalse(game.players[0].is_bot)
        self.assertFalse(game.players[0].connected)
        self.assertTrue(game.players[1].is_bot)
        self.assertTrue(game.players[1].connected)
        self.assertIsInstance(game.players[1].brain, HeuristicPlayer)
        self.assertIsInstance(game.players[2].brain, RandomPlayer)


if __name__ == '__main__':
    unittest.main(verbosity=2)
