"""Perform tests for the REST endpoint `/trials`"""
import copy
import datetime
import random

import falcon

from orion.core.io import experiment_builder
from orion.core.worker.trial import Trial
from orion.storage.base import get_storage

base_experiment = dict(
    name="experiment-name",
    space={"x": "uniform(0, 200)"},
    metadata={
        "user": "test-user",
        "orion_version": "x.y.z",
        "datetime": datetime.datetime(1, 1, 1),
        "VCS": {
            "type": "git",
            "is_dirty": False,
            "HEAD_sha": "test",
            "active_branch": None,
            "diff_sha": "diff",
        },
    },
    refers={},
    version=1,
    pool_size=1,
    max_trials=10,
    working_dir="",
    algorithms={"random": {"seed": 1}},
)

base_trial = {
    "experiment": None,
    "status": "new",
    "worker": None,
    "submit_time": datetime.datetime(1, 1, 1),
    "start_time": datetime.datetime(1, 1, 1, second=10),
    "end_time": datetime.datetime(1, 1, 2),
    "heartbeat": None,
    "results": [
        {"name": "loss", "type": "objective", "value": 0.05},
        {"name": "a", "type": "statistic", "value": 10},
        {"name": "b", "type": "statistic", "value": 5},
    ],
    "params": [
        {"name": "x", "type": "real", "value": 10.0},
    ],
}


def add_experiment(**kwargs):
    """Adds experiment to the dummy orion instance"""
    base_experiment.update(copy.deepcopy(kwargs))
    experiment_builder.build(
        branching=dict(branch_from=base_experiment["name"]), **base_experiment
    )


def add_trial(experiment: int, status: str = None, **kwargs):
    """
    Add trials to the dummy orion instance

    Parameters
    ----------
    experiment
        The ID of the experiment (stored in Experiment._id)
    status
        The status of the trial to add.
    **kwargs
        Any other attribute to modify from the base trial config.
    """
    if not status:
        status = random.choice(Trial.allowed_stati)

    kwargs["experiment"] = experiment
    kwargs["status"] = status

    base_trial.update(copy.deepcopy(kwargs))
    get_storage().register_trial(Trial(**base_trial))


def test_root_endpoint_not_supported(client):
    """Tests that the server return a 404 when accessing trials/ with no experiment"""
    response = client.simulate_get("/trials")

    assert response.status == "404 Not Found"
    if falcon.__version__ < "3.0.0":
        assert not response.json
    else:
        assert response.json == {"title": "404 Not Found"}


class TestTrialCollection:
    """Tests trials/:experiment_name"""

    def test_trials_for_unknown_experiment(self, client):
        """Tests that an unknown experiment returns a not found error"""
        response = client.simulate_get("/trials/unknown-experiment")

        assert response.status == "404 Not Found"
        assert response.json == {
            "title": "Experiment not found",
            "description": 'Experiment "unknown-experiment" does not exist',
        }

    def test_unknown_parameter(self, client):
        """
        Tests that if an unknown parameter is specified in
        the query string, an error is returned even if the experiment doesn't exist.
        """
        expected_error_message = (
            'Parameter "unknown" is not supported. '
            "Expected one of ['ancestors', 'status', 'version']."
        )

        response = client.simulate_get("/trials/a?unknown=true")

        assert response.status == "400 Bad Request"
        assert response.json == {
            "title": "Invalid parameter",
            "description": expected_error_message,
        }

        add_experiment(name="a", version=1, _id=1)

        response = client.simulate_get("/trials/a?unknown=true")

        assert response.status == "400 Bad Request"
        assert response.json == {
            "title": "Invalid parameter",
            "description": expected_error_message,
        }

    def test_trials_for_latest_version(self, client):
        """Tests that it returns the trials of the latest version of the experiment"""
        add_experiment(name="a", version=1, _id=1)
        add_experiment(name="a", version=2, _id=2)

        add_trial(experiment=1, id_override="00")
        add_trial(experiment=2, id_override="01")
        add_trial(experiment=1, id_override="02")
        add_trial(experiment=2, id_override="03")

        response = client.simulate_get("/trials/a")

        assert response.status == "200 OK"
        assert response.json == [{"id": "01"}, {"id": "03"}]

    def test_trials_for_specific_version(self, client):
        """Tests specific version of experiment"""
        add_experiment(name="a", version=1, _id=1)
        add_experiment(name="a", version=2, _id=2)
        add_experiment(name="a", version=3, _id=3)

        add_trial(experiment=1, id_override="00")
        add_trial(experiment=2, id_override="01")
        add_trial(experiment=3, id_override="02")

        # Happy case
        response = client.simulate_get("/trials/a?version=2")

        assert response.status == "200 OK"
        assert response.json == [{"id": "01"}]

        # Version doesn't exist
        response = client.simulate_get("/trials/a?version=4")

        assert response.status == "404 Not Found"
        assert response.json == {
            "title": "Experiment not found",
            "description": 'Experiment "a" has no version "4"',
        }

    def test_trials_for_all_versions(self, client):
        """Tests that trials from all ancestors are shown"""
        add_experiment(name="a", version=1, _id=1)
        add_experiment(name="a", version=2, _id=2)
        add_experiment(name="a", version=3, _id=3)

        add_trial(experiment=1, id_override="00")
        add_trial(experiment=2, id_override="01")
        add_trial(experiment=3, id_override="02")

        # Happy case default
        response = client.simulate_get("/trials/a?ancestors=true")

        assert response.status == "200 OK"
        assert response.json == [{"id": "00"}, {"id": "01"}, {"id": "02"}]

        # Happy case explicitly false (call latest version test)
        response = client.simulate_get("/trials/a?ancestors=false")

        assert response.status == "200 OK"
        assert response.json == [{"id": "02"}]

        # Not a boolean parameter
        response = client.simulate_get("/trials/a?ancestors=42")

        assert response.status == "400 Bad Request"
        assert response.json == {
            "title": "Invalid parameter",
            "description": 'The "ancestors" parameter is invalid. '
            'The value of the parameter must be "true" or "false".',
        }

    def test_trials_by_status(self, client):
        """Tests that trials are returned"""
        add_experiment(name="a", version=1, _id=1)

        # There exist no trial of the given status in an empty experiment
        response = client.simulate_get("/trials/a?status=completed")

        assert response.status == "200 OK"
        assert response.json == []

        # There exist no trial of the given status while other status are present
        add_trial(experiment=1, id_override="00", status="broken")

        response = client.simulate_get("/trials/a?status=completed")
        assert response.status == "200 OK"
        assert response.json == []

        # There exist at least one trial of the given status
        add_trial(experiment=1, id_override="01", status="completed")

        response = client.simulate_get("/trials/a?status=completed")

        assert response.status == "200 OK"
        assert response.json == [{"id": "01"}]

        # Status doesn't exist
        response = client.simulate_get("/trials/a?status=invalid")

        assert response.status == "400 Bad Request"
        error_message = (
            "Invalid status value. "
            "Expected one of "
            "['new', 'reserved', 'suspended', 'completed', 'interrupted', 'broken']"
        )
        assert response.json == {
            "title": "Invalid parameter",
            "description": 'The "status" parameter is invalid. '
            "The value of the parameter must be one of "
            "['new', 'reserved', 'suspended', 'completed', "
            "'interrupted', 'broken']",
        }

    def test_trials_by_from_specific_version_by_status_with_ancestors(self, client):
        """Tests that mixing parameters work as intended"""
        add_experiment(name="a", version=1, _id=1)
        add_experiment(name="b", version=1, _id=2)
        add_experiment(name="a", version=2, _id=3)
        add_experiment(name="a", version=3, _id=4)

        add_trial(experiment=1, id_override="00", status="completed")
        add_trial(experiment=3, id_override="01", status="broken")
        add_trial(experiment=3, id_override="02", status="completed")
        add_trial(experiment=2, id_override="03", status="completed")

        response = client.simulate_get(
            "/trials/a?ancestors=true&version=2&status=completed"
        )

        assert response.status == "200 OK"
        assert response.json == [{"id": "00"}, {"id": "02"}]


class TestTrialItem:
    """Tests trials/:experiment_name/:trial_id"""

    def test_unknown_experiment(self, client):
        """Tests that an unknown experiment returns a not found error"""
        response = client.simulate_get("/trials/unknown-experiment/a-trial")

        assert response.status == "404 Not Found"
        assert response.json == {
            "title": "Experiment not found",
            "description": 'Experiment "unknown-experiment" does not exist',
        }

    def test_unknown_trial(self, client):
        """Tests that an unknown experiment returns a not found error"""
        add_experiment(name="a", version=1, _id=1)

        response = client.simulate_get("/trials/a/unknown-trial")

        assert response.status == "404 Not Found"
        assert response.json == {
            "title": "Trial not found",
            "description": 'Trial "unknown-trial" does not exist',
        }

    def test_get_trial(self, client):
        """Tests that an existing trial is returned according to the API specification"""
        add_experiment(name="a", version=1, _id=1)
        add_trial(experiment=1, id_override="00", status="completed")
        add_trial(experiment=1, id_override="01", status="completed")

        response = client.simulate_get("/trials/a/01")

        assert response.status == "200 OK"
        assert response.json == {
            "id": "01",
            "submitTime": "0001-01-01 00:00:00",
            "startTime": "0001-01-01 00:00:10",
            "endTime": "0001-01-02 00:00:00",
            "parameters": {"x": 10.0},
            "objective": 0.05,
            "statistics": {"a": 10, "b": 5},
        }
