from __future__ import annotations

import gymnasium as gym
import numpy as np

from blokus_gym import (
    STANDARD_PIECES,
    ActionMaskWrapper,
    BlokusEnv,
    BlokusMultiAgentEnv,
    GreedyBot,
)


class TestBlokusEnv:
    def test_env_creation(self):
        env = BlokusEnv(num_players=4, board_size=20)
        assert env.num_players == 4
        assert env.board_size == 20

    def test_observation_space(self):
        env = BlokusEnv(num_players=4, board_size=20)
        obs, _ = env.reset(seed=42)
        assert "board" in obs
        assert "pieces" in obs
        assert "corners" in obs
        assert obs["board"].shape == (20, 20)
        assert obs["pieces"].shape == (21,)
        assert obs["corners"].shape == (20, 20)

    def test_action_space(self):
        env = BlokusEnv(num_players=4, board_size=20)
        assert env.action_space.shape == ()
        assert env.action_space.n > 0

    def test_reset_returns_obs_and_info(self):
        env = BlokusEnv(num_players=4, board_size=20)
        obs, info = env.reset(seed=42)
        assert isinstance(obs, dict)
        assert isinstance(info, dict)
        assert "action_mask" in info

    def test_action_mask_valid(self):
        env = BlokusEnv(num_players=4, board_size=20)
        obs, info = env.reset(seed=42)
        mask = info["action_mask"]
        assert mask.dtype == bool
        assert mask.sum() > 0  # Should have valid moves

    def test_step_with_valid_action(self):
        env = BlokusEnv(num_players=4, board_size=20)
        obs, info = env.reset(seed=42)
        valid_actions = np.where(info["action_mask"])[0]
        action = valid_actions[0]
        obs, reward, terminated, truncated, info = env.step(action)
        assert isinstance(reward, float)
        assert isinstance(terminated, bool)
        assert isinstance(truncated, bool)

    def test_step_with_invalid_action(self):
        env = BlokusEnv(num_players=4, board_size=20)
        obs, info = env.reset(seed=42)
        # Use an action that's likely invalid
        obs, reward, terminated, truncated, info = env.step(env.action_space.n - 1)
        assert reward < 0  # Penalty for invalid action
        assert terminated  # Should end episode

    def test_two_player_env(self):
        env = BlokusEnv(num_players=2, board_size=14)
        obs, info = env.reset(seed=42)
        assert env.num_players == 2

    def test_three_player_env(self):
        env = BlokusEnv(num_players=3, board_size=20)
        obs, info = env.reset(seed=42)
        assert env.num_players == 3

    def test_custom_pieces(self):
        custom = [p for p in STANDARD_PIECES if p.size < 5]
        env = BlokusEnv(num_players=2, board_size=10, pieces=custom)
        obs, info = env.reset(seed=42)
        assert obs["pieces"].shape == (len(custom),)

    def test_greedy_bot_opponent(self):
        env = BlokusEnv(num_players=2, board_size=7, bot_type=GreedyBot)
        obs, info = env.reset(seed=42)
        assert env.bots[1] is not None

    def test_game_over(self):
        env = BlokusEnv(num_players=2, board_size=5, max_steps=10)
        obs, info = env.reset(seed=42)
        terminated = False
        truncated = False
        steps = 0
        while not (terminated or truncated) and steps < 50:
            valid = np.where(info["action_mask"])[0]
            if len(valid) == 0:
                break
            obs, reward, terminated, truncated, info = env.step(valid[0])
            steps += 1

    def test_seed_reproducibility(self):
        env1 = BlokusEnv(num_players=2, board_size=7)
        env2 = BlokusEnv(num_players=2, board_size=7)
        obs1, _ = env1.reset(seed=42)
        obs2, _ = env2.reset(seed=42)
        np.testing.assert_array_equal(obs1["board"], obs2["board"])

    def test_render_text(self):
        env = BlokusEnv(num_players=2, board_size=7, render_mode="ansi")
        obs, info = env.reset(seed=42)
        result = env.render()
        assert isinstance(result, str)


class TestActionMaskWrapper:
    def test_wrapper_creation(self):
        env = BlokusEnv(num_players=2, board_size=7)
        env = ActionMaskWrapper(env)
        obs, info = env.reset(seed=42)
        assert "observation" in obs
        assert "action_mask" in obs

    def test_wrapper_step(self):
        env = BlokusEnv(num_players=2, board_size=7)
        env = ActionMaskWrapper(env)
        obs, info = env.reset(seed=42)
        valid = np.where(obs["action_mask"])[0]
        obs, reward, terminated, truncated, info = env.step(valid[0])
        assert "observation" in obs
        assert "action_mask" in obs


class TestMultiAgentEnv:
    def test_env_creation(self):
        env = BlokusMultiAgentEnv(num_players=2, board_size=7)
        assert env.num_players == 2
        assert len(env.agents) == 2

    def test_reset(self):
        env = BlokusMultiAgentEnv(num_players=2, board_size=7)
        obs, info = env.reset(seed=42)
        assert "player_0" in obs

    def test_step(self):
        env = BlokusMultiAgentEnv(num_players=2, board_size=7)
        obs, info = env.reset(seed=42)
        mask = env.get_action_mask("player_0")
        valid = np.where(mask)[0]
        obs, rewards, terminations, truncations, infos = env.step(valid[0])
        assert isinstance(rewards, dict)
        assert isinstance(terminations, dict)

    def test_observation_space(self):
        env = BlokusMultiAgentEnv(num_players=2, board_size=7)
        obs, _ = env.reset(seed=42)
        for agent in env.agents:
            if agent in obs:
                assert "board" in obs[agent]

    def test_action_space(self):
        env = BlokusMultiAgentEnv(num_players=2, board_size=7)
        env.reset(seed=42)
        space = env.action_space("player_0")
        assert space.n > 0

    def test_four_player(self):
        env = BlokusMultiAgentEnv(num_players=4, board_size=7)
        obs, info = env.reset(seed=42)
        assert len(env.agents) == 4


class TestRegisteredEnvs:
    def test_blokus_v0(self):
        env = gym.make("Blokus-v0")
        obs, info = env.reset(seed=42)
        assert obs["board"].shape == (20, 20)

    def test_blokus_duo_v0(self):
        env = gym.make("BlokusDuo-v0")
        obs, info = env.reset(seed=42)
        assert obs["board"].shape == (14, 14)

    def test_blokus_junior_v0(self):
        env = gym.make("BlokusJunior-v0")
        obs, info = env.reset(seed=42)
        assert obs["board"].shape == (14, 14)

    def test_blokus_simple_v0(self):
        env = gym.make("BlokusSimple-v0")
        obs, info = env.reset(seed=42)
        assert obs["board"].shape == (7, 7)

    def test_blokus_greedy_v0(self):
        env = gym.make("BlokusGreedy-v0")
        obs, info = env.reset(seed=42)
        assert obs["board"].shape == (20, 20)
