#!/usr/bin/env python
# coding=utf-8
from __future__ import division, print_function, unicode_literals, absolute_import

import datetime
import os
import tempfile
import io

import pytest

tinydb = pytest.importorskip("tinydb")
hashfs = pytest.importorskip("hashfs")

from tinydb import Query

from sacred.dependencies import get_digest
from sacred.observers.tinydb_hashfs import TinyDbObserver, TinyDbReader


# Utilities and fixtures
def sample_run():

    T1 = datetime.datetime(1999, 5, 4, 3, 2, 1, 0)

    exp = {
        "name": "test_exp",
        "sources": [],
        "doc": "",
        "base_dir": os.path.join(os.path.dirname(__file__), "..", ".."),
        "dependencies": ["sacred==0.7b0"],
    }
    host = {"hostname": "test_host", "cpu_count": 1, "python_version": "3.4"}
    config = {"config": "True", "foo": "bar", "answer": 42}
    command = "run"
    meta_info = {"comment": "test run"}
    sample_run = {
        "_id": "FED235DA13",
        "ex_info": exp,
        "command": command,
        "host_info": host,
        "start_time": T1,
        "config": config,
        "meta_info": meta_info,
    }

    filename = "setup.py"
    md5 = get_digest(filename)
    sample_run["ex_info"]["sources"] = [[filename, md5]]

    return sample_run


def run_test_experiment(exp_name, exp_id, root_dir):

    T2 = datetime.datetime(1999, 5, 5, 5, 5, 5, 5)
    T3 = datetime.datetime(1999, 5, 5, 6, 6, 6, 6)

    run_date = sample_run()
    run_date["ex_info"]["name"] = exp_name
    run_date["_id"] = exp_id

    # Create
    tinydb_obs = TinyDbObserver.create(path=root_dir)

    # Start exp 1
    tinydb_obs.started_event(**run_date)

    # Heartbeat
    info = {"my_info": [1, 2, 3], "nr": 7}
    outp = "some output"
    tinydb_obs.heartbeat_event(info=info, captured_out=outp, beat_time=T2, result=7)
    # Add Artifact
    filename = "sacred/__about__.py"
    name = "about"
    tinydb_obs.artifact_event(name, filename)

    # Add Resource
    filename = "sacred/__init__.py"
    tinydb_obs.resource_event(filename)

    # Complete
    tinydb_obs.completed_event(stop_time=T3, result=42)

    return tinydb_obs


def strip_file_handles(results):
    """Return a database result set with all file handle objects removed.abs

    Utility function to aid comparison of database entries. As file handles are
    created newly each object, these are always different so can be excluded.
    """

    if not isinstance(results, (list, tuple)):
        results = [results]

    cleaned_results = []
    for result in results:
        sources = result["experiment"]["sources"]
        artifacts = result["artifacts"]
        resources = result["resources"]
        if sources:
            for src in sources:
                if isinstance(src[-1], io.BufferedReader):
                    del src[-1]
        if artifacts:
            for art in artifacts:
                if isinstance(art[-1], io.BufferedReader):
                    del art[-1]
        if resources:
            for res in resources:
                if isinstance(res[-1], io.BufferedReader):
                    del res[-1]
        cleaned_results.append(result)

    return cleaned_results


# TinyDbReader Tests
def test_tinydb_reader_loads_db_and_fs(tmpdir):

    root = tmpdir.strpath
    tinydb_obs = run_test_experiment(exp_name="exp1", exp_id="1234", root_dir=root)
    tinydb_reader = TinyDbReader(root)

    assert tinydb_obs.fs.root == tinydb_reader.fs.root
    # Different file handles are used in each object so compar based on str
    # representation
    assert str(tinydb_obs.runs.all()[0]) == str(tinydb_reader.runs.all()[0])


def test_tinydb_reader_raises_exceptions(tmpdir):
    with pytest.raises(IOError):
        TinyDbReader("foo")


def test_fetch_metadata_function_with_indices(tmpdir):
    sample_run_ = sample_run()
    # Setup and run three experiments
    root = tmpdir.strpath
    tinydb_obs = run_test_experiment(
        exp_name="experiment 1 alpha", exp_id="1234", root_dir=root
    )
    tinydb_obs = run_test_experiment(
        exp_name="experiment 2 beta", exp_id="5678", root_dir=root
    )
    tinydb_obs = run_test_experiment(
        exp_name="experiment 3 alpha", exp_id="9990", root_dir=root
    )

    tinydb_reader = TinyDbReader(root)

    # Test fetch by indices
    res = tinydb_reader.fetch_metadata(indices=-1)
    res2 = tinydb_reader.fetch_metadata(indices=[-1])
    assert strip_file_handles(res) == strip_file_handles(res2)
    res3 = tinydb_reader.fetch_metadata(indices=[0, -1])
    assert len(res3) == 2

    exp1_res = tinydb_reader.fetch_metadata(indices=0)
    assert len(exp1_res) == 1
    assert exp1_res[0]["experiment"]["name"] == "experiment 1 alpha"
    assert exp1_res[0]["_id"] == "1234"

    # Test Exception
    with pytest.raises(ValueError):
        tinydb_reader.fetch_metadata(indices=4)

    # Test returned values
    exp1 = strip_file_handles(exp1_res)[0]

    sample_run_["ex_info"]["name"] = "experiment 1 alpha"
    sample_run_["ex_info"]["sources"] = [["setup.py", get_digest("setup.py")]]

    assert exp1 == {
        "_id": "1234",
        "experiment": sample_run_["ex_info"],
        "format": tinydb_obs.VERSION,
        "command": sample_run_["command"],
        "host": sample_run_["host_info"],
        "start_time": sample_run_["start_time"],
        "heartbeat": datetime.datetime(1999, 5, 5, 5, 5, 5, 5),
        "info": {"my_info": [1, 2, 3], "nr": 7},
        "captured_out": "some output",
        "artifacts": [
            ["about", "sacred/__about__.py", get_digest("sacred/__about__.py")]
        ],
        "config": sample_run_["config"],
        "meta": sample_run_["meta_info"],
        "status": "COMPLETED",
        "resources": [["sacred/__init__.py", get_digest("sacred/__init__.py")]],
        "result": 42,
        "stop_time": datetime.datetime(1999, 5, 5, 6, 6, 6, 6),
    }


def test_fetch_metadata_function_with_exp_name(tmpdir):

    # Setup and run three experiments
    root = tmpdir.strpath
    run_test_experiment(exp_name="experiment 1 alpha", exp_id="1234", root_dir=root)
    run_test_experiment(exp_name="experiment 2 beta", exp_id="5678", root_dir=root)
    run_test_experiment(exp_name="experiment 3 alpha", exp_id="9990", root_dir=root)

    tinydb_reader = TinyDbReader(root)

    # Test Fetch by exp name
    res1 = tinydb_reader.fetch_metadata(exp_name="alpha")
    assert len(res1) == 2
    res2 = tinydb_reader.fetch_metadata(exp_name="experiment 1")
    assert len(res2) == 1
    assert res2[0]["experiment"]["name"] == "experiment 1 alpha"
    res2 = tinydb_reader.fetch_metadata(exp_name="foo")
    assert len(res2) == 0


def test_fetch_metadata_function_with_querry(tmpdir):

    # Setup and run three experiments
    root = tmpdir.strpath
    run_test_experiment(exp_name="experiment 1 alpha", exp_id="1234", root_dir=root)
    run_test_experiment(exp_name="experiment 2 beta", exp_id="5678", root_dir=root)
    run_test_experiment(
        exp_name="experiment 3 alpha beta", exp_id="9990", root_dir=root
    )

    tinydb_reader = TinyDbReader(root)

    record = Query()

    exp1_query = record.experiment.name.matches(".*alpha$")

    exp3_query = (record.experiment.name.search("alpha")) & (record._id == "9990")

    # Test Fetch by Tinydb Query
    res1 = tinydb_reader.fetch_metadata(query=exp1_query)
    assert len(res1) == 1
    assert res1[0]["experiment"]["name"] == "experiment 1 alpha"

    res2 = tinydb_reader.fetch_metadata(
        query=record.experiment.name.search("experiment [23]")
    )
    assert len(res2) == 2

    res3 = tinydb_reader.fetch_metadata(query=exp3_query)
    assert len(res3) == 1
    assert res3[0]["experiment"]["name"] == "experiment 3 alpha beta"

    # Test Exception
    with pytest.raises(ValueError):
        tinydb_reader.fetch_metadata()


def test_search_function(tmpdir):

    # Setup and run three experiments
    root = tmpdir.strpath
    run_test_experiment(exp_name="experiment 1 alpha", exp_id="1234", root_dir=root)
    run_test_experiment(exp_name="experiment 2 beta", exp_id="5678", root_dir=root)
    run_test_experiment(
        exp_name="experiment 3 alpha beta", exp_id="9990", root_dir=root
    )

    tinydb_reader = TinyDbReader(root)

    # Test Fetch by Tinydb Query in search function
    record = Query()
    q = record.experiment.name.search("experiment [23]")

    res = tinydb_reader.search(q)
    assert len(res) == 2
    res2 = tinydb_reader.fetch_metadata(query=q)
    assert strip_file_handles(res) == strip_file_handles(res2)


def test_fetch_files_function(tmpdir):
    # Setup and run three experiments
    root = tmpdir.strpath
    run_test_experiment(exp_name="experiment 1 alpha", exp_id="1234", root_dir=root)
    run_test_experiment(exp_name="experiment 2 beta", exp_id="5678", root_dir=root)
    run_test_experiment(
        exp_name="experiment 3 alpha beta", exp_id="9990", root_dir=root
    )

    tinydb_reader = TinyDbReader(root)

    res = tinydb_reader.fetch_files(indices=0)
    assert len(res) == 1
    assert list(res[0]["artifacts"].keys()) == ["about"]
    assert isinstance(res[0]["artifacts"]["about"], io.BufferedReader)
    assert res[0]["date"] == datetime.datetime(1999, 5, 4, 3, 2, 1)
    assert res[0]["exp_id"] == "1234"
    assert res[0]["exp_name"] == "experiment 1 alpha"
    assert list(res[0]["resources"].keys()) == ["sacred/__init__.py"]
    assert isinstance(res[0]["resources"]["sacred/__init__.py"], io.BufferedReader)
    assert list(res[0]["sources"].keys()) == ["setup.py"]
    assert isinstance(res[0]["sources"]["setup.py"], io.BufferedReader)


def test_fetch_report_function(tmpdir):

    # Setup and run three experiments
    root = tmpdir.strpath
    run_test_experiment(exp_name="experiment 1 alpha", exp_id="1234", root_dir=root)
    run_test_experiment(exp_name="experiment 2 beta", exp_id="5678", root_dir=root)
    run_test_experiment(
        exp_name="experiment 3 alpha beta", exp_id="9990", root_dir=root
    )

    tinydb_reader = TinyDbReader(root)

    res = tinydb_reader.fetch_report(indices=0)

    target = """
-------------------------------------------------
Experiment: experiment 1 alpha
-------------------------------------------------
ID: 1234
Date: Tue 04 May 1999    Duration: 27:04:05.0

Parameters:
    answer: 42
    config: True
    foo: bar

Result:
    42

Dependencies:
    sacred==0.7b0

Resources:
    sacred/__init__.py

Source Files:
    setup.py

Outputs:
    about
"""

    assert res[0] == target