from pathlib import Path
from typing import Any

import pytest
import pytest_mock
import yaml

from vivarium.framework.configuration import (
    DEFAULT_PLUGINS,
    ConfigurationError,
    _get_default_specification,
    build_model_specification,
    build_simulation_configuration,
    validate_model_specification_file,
)


def test_get_default_specification_user_config(
    mocker: pytest_mock.MockFixture, test_user_config: Path
) -> None:
    expand_user_mock = mocker.patch("vivarium.framework.configuration.Path.expanduser")
    expand_user_mock.return_value = test_user_config

    default_spec = _get_default_specification()

    assert expand_user_mock.called_once_with("~/vivarium.yaml")

    with test_user_config.open() as f:
        data = {"configuration": yaml.full_load(f)}

    data.update(DEFAULT_PLUGINS)
    data.update({"components": {}})

    assert default_spec.to_dict() == data


def test_get_default_specification_no_user_config(
    mocker: pytest_mock.MockFixture, test_data_dir: Path
) -> None:
    user_config = test_data_dir / "oh_no_nothing_here.yaml"

    expand_user_mock = mocker.patch("vivarium.framework.configuration.Path.expanduser")
    expand_user_mock.return_value = user_config

    default_spec = _get_default_specification()

    assert expand_user_mock.called_once_with("~/vivarium.yaml")
    data: dict[str, dict[Any, Any]] = {"components": {}, "configuration": {}}
    data.update(DEFAULT_PLUGINS)

    assert default_spec.to_dict() == data


def test_validate_model_specification_failures(
    mocker: pytest_mock.MockFixture, test_data_dir: Path, test_spec: Path
) -> None:
    with pytest.raises(ConfigurationError):
        validate_model_specification_file("made_up_file.yaml")

    with pytest.raises(ConfigurationError):
        model_spec = test_data_dir / "bad_model_specification.txt"
        validate_model_specification_file(model_spec)

    with test_spec.open() as f:
        spec_dict = yaml.full_load(f)
    spec_dict.update({"invalid_key": "some_value"})
    load_mock = mocker.patch("vivarium.framework.configuration.yaml.full_load")
    load_mock.return_value = spec_dict
    with pytest.raises(ConfigurationError):
        validate_model_specification_file(test_spec)


def test_validate_model_specification(test_spec: Path) -> None:
    validate_model_specification_file(test_spec)


def test_build_simulation_configuration(
    mocker: pytest_mock.MockFixture, test_user_config: Path
) -> None:
    expand_user_mock = mocker.patch("vivarium.framework.configuration.Path.expanduser")
    expand_user_mock.return_value = test_user_config

    config = build_simulation_configuration()

    assert expand_user_mock.called_once_with("~/vivarium.yaml")

    with test_user_config.open() as f:
        data = yaml.full_load(f)

    assert config.to_dict() == data


def test_build_model_specification_failure(
    mocker: pytest_mock.MockFixture, test_data_dir: Path, test_spec: Path
) -> None:
    with pytest.raises(ConfigurationError):
        build_model_specification("made_up_file.yaml")

    with pytest.raises(ConfigurationError):
        model_spec = test_data_dir / "bad_model_specification.txt"
        build_model_specification(model_spec)

    with test_spec.open() as f:
        spec_dict = yaml.full_load(f)
    spec_dict.update({"invalid_key": "some_value"})
    load_mock = mocker.patch("vivarium.framework.configuration.yaml.full_load")
    load_mock.return_value = spec_dict
    with pytest.raises(ConfigurationError):
        build_model_specification(str(test_spec))


def test_build_model_specification(
    mocker: pytest_mock.MockFixture, test_spec: Path, test_user_config: Path
) -> None:
    expand_user_mock = mocker.patch("vivarium.framework.configuration.Path.expanduser")
    expand_user_mock.return_value = test_user_config
    loaded_model_spec = build_model_specification(test_spec)

    test_data = DEFAULT_PLUGINS.copy()

    with test_spec.open() as f:
        model_data = yaml.full_load(f)

    test_data.update(model_data)

    with test_user_config.open() as f:
        user_data = yaml.full_load(f)

    test_data["configuration"].update(user_data)

    assert loaded_model_spec.to_dict() == test_data
