import pytest
from unittest.mock import patch, call, Mock

from mlagents.trainers.meta_curriculum import MetaCurriculum
from mlagents.trainers.exception import MetaCurriculumError


class MetaCurriculumTest(MetaCurriculum):
    """This class allows us to test MetaCurriculum objects without calling
    MetaCurriculum's __init__ function.
    """

    def __init__(self, brains_to_curriculums):
        self._brains_to_curriculums = brains_to_curriculums


@pytest.fixture
def default_reset_parameters():
    return {"param1": 1, "param2": 2, "param3": 3}


@pytest.fixture
def more_reset_parameters():
    return {"param4": 4, "param5": 5, "param6": 6}


@pytest.fixture
def measure_vals():
    return {"Brain1": 0.2, "Brain2": 0.3}


@pytest.fixture
def reward_buff_sizes():
    return {"Brain1": 7, "Brain2": 8}


@patch("mlagents.trainers.Curriculum.get_config", return_value={})
@patch("mlagents.trainers.Curriculum.__init__", return_value=None)
@patch("os.listdir", return_value=["Brain1.json", "Brain2.json"])
def test_init_meta_curriculum_happy_path(
    listdir, mock_curriculum_init, mock_curriculum_get_config, default_reset_parameters
):
    meta_curriculum = MetaCurriculum("test/", default_reset_parameters)

    assert len(meta_curriculum.brains_to_curriculums) == 2

    assert "Brain1" in meta_curriculum.brains_to_curriculums
    assert "Brain2" in meta_curriculum.brains_to_curriculums

    calls = [
        call("test/Brain1.json", default_reset_parameters),
        call("test/Brain2.json", default_reset_parameters),
    ]

    mock_curriculum_init.assert_has_calls(calls)


@patch("os.listdir", side_effect=NotADirectoryError())
def test_init_meta_curriculum_bad_curriculum_folder_raises_error(listdir):
    with pytest.raises(MetaCurriculumError):
        MetaCurriculum("test/", default_reset_parameters)


@patch("mlagents.trainers.Curriculum")
@patch("mlagents.trainers.Curriculum")
def test_set_lesson_nums(curriculum_a, curriculum_b):
    meta_curriculum = MetaCurriculumTest(
        {"Brain1": curriculum_a, "Brain2": curriculum_b}
    )

    meta_curriculum.lesson_nums = {"Brain1": 1, "Brain2": 3}

    assert curriculum_a.lesson_num == 1
    assert curriculum_b.lesson_num == 3


@patch("mlagents.trainers.Curriculum")
@patch("mlagents.trainers.Curriculum")
def test_increment_lessons(curriculum_a, curriculum_b, measure_vals):
    meta_curriculum = MetaCurriculumTest(
        {"Brain1": curriculum_a, "Brain2": curriculum_b}
    )

    meta_curriculum.increment_lessons(measure_vals)

    curriculum_a.increment_lesson.assert_called_with(0.2)
    curriculum_b.increment_lesson.assert_called_with(0.3)


@patch("mlagents.trainers.Curriculum")
@patch("mlagents.trainers.Curriculum")
def test_increment_lessons_with_reward_buff_sizes(
    curriculum_a, curriculum_b, measure_vals, reward_buff_sizes
):
    curriculum_a.min_lesson_length = 5
    curriculum_b.min_lesson_length = 10
    meta_curriculum = MetaCurriculumTest(
        {"Brain1": curriculum_a, "Brain2": curriculum_b}
    )

    meta_curriculum.increment_lessons(measure_vals, reward_buff_sizes=reward_buff_sizes)

    curriculum_a.increment_lesson.assert_called_with(0.2)
    curriculum_b.increment_lesson.assert_not_called()


@patch("mlagents.trainers.Curriculum")
@patch("mlagents.trainers.Curriculum")
def test_set_all_curriculums_to_lesson_num(curriculum_a, curriculum_b):
    meta_curriculum = MetaCurriculumTest(
        {"Brain1": curriculum_a, "Brain2": curriculum_b}
    )

    meta_curriculum.set_all_curriculums_to_lesson_num(2)

    assert curriculum_a.lesson_num == 2
    assert curriculum_b.lesson_num == 2


@patch("mlagents.trainers.Curriculum")
@patch("mlagents.trainers.Curriculum")
def test_get_config(
    curriculum_a, curriculum_b, default_reset_parameters, more_reset_parameters
):
    curriculum_a.get_config.return_value = default_reset_parameters
    curriculum_b.get_config.return_value = default_reset_parameters
    meta_curriculum = MetaCurriculumTest(
        {"Brain1": curriculum_a, "Brain2": curriculum_b}
    )

    assert meta_curriculum.get_config() == default_reset_parameters

    curriculum_b.get_config.return_value = more_reset_parameters

    new_reset_parameters = dict(default_reset_parameters)
    new_reset_parameters.update(more_reset_parameters)

    assert meta_curriculum.get_config() == new_reset_parameters
