package de.dorianscholz.openlibre;

import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.util.Log;

import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

import de.dorianscholz.openlibre.model.GlucoseData;
import de.dorianscholz.openlibre.model.RawTagData;
import de.dorianscholz.openlibre.model.ReadingData;
import io.realm.Realm;
import io.realm.RealmResults;
import io.realm.Sort;

import static de.dorianscholz.openlibre.OpenLibre.parseRawData;
import static de.dorianscholz.openlibre.OpenLibre.realmConfigProcessedData;
import static de.dorianscholz.openlibre.OpenLibre.realmConfigRawData;
import static de.dorianscholz.openlibre.OpenLibre.setupRealm;
import static de.dorianscholz.openlibre.model.ReadingData.historyIntervalInMinutes;
import static de.dorianscholz.openlibre.model.ReadingData.numHistoryValues;
import static de.dorianscholz.openlibre.model.ReadingData.numTrendValues;
import static java.lang.Math.abs;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.everyItem;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThanOrEqualTo;

@RunWith(AndroidJUnit4.class)
public class ReadingDataTest {
    private static final int MAX_READINGS_TO_TEST = 1000;

    private Realm realmRawData;
    private Realm realmProcessedData;
    private ArrayList<ReadingData> readingDataList = new ArrayList<>();
    private ArrayList<RawTagData> rawTagDataList = new ArrayList<>();

    @Before
    public void setUp() throws Exception {
        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
        Realm.init(context);
        setupRealm(context);

        realmRawData = Realm.getInstance(realmConfigRawData);
    }

    private void parseTestData() {
        // open empty processed data realm for use in parsing data
        Realm.deleteRealm(realmConfigProcessedData);
        realmProcessedData = Realm.getInstance(realmConfigProcessedData);

        // get all raw data
        RealmResults<RawTagData> rawTags = realmRawData.where(RawTagData.class).findAllSorted(RawTagData.DATE, Sort.ASCENDING);

        // reduce data set to just the raw data of the most recent sensor
        String tagId = rawTags.last().getTagId();
        rawTags = rawTags.where().equalTo(RawTagData.TAG_ID, tagId).findAllSorted(RawTagData.DATE, Sort.ASCENDING);

        // reduce data set further to only MAX_READINGS_TO_TEST sensor readings
        for (int i = 0; i < min(MAX_READINGS_TO_TEST, rawTags.size()); i++) {
            addDataIfValid(rawTags.get(i));
        }

        /*
        // add oldest readings of sensor
        for (int i = 0; i < min(MAX_READINGS_TO_TEST / 2, rawTags.size() / 2); i++) {
            addDataIfValid(rawTags.get(i));
        }
        // add newest readings of sensor
        for (int i = max(rawTags.size() / 2, rawTags.size() - 1 - MAX_READINGS_TO_TEST / 2); i < rawTags.size() - 1; i++) {
            addDataIfValid(rawTags.get(i));
        }
        */

        assertThat(realmRawData.isEmpty(), is(false));
        assertThat(readingDataList.size(), greaterThan(0));
    }

    private void addDataIfValid(RawTagData rawTagData) {
        // check if sensor has been initialized and is not yet over due date
        if (rawTagData.getSensorAgeInMinutes() < numHistoryValues * historyIntervalInMinutes ||
                rawTagData.getSensorAgeInMinutes() > TimeUnit.DAYS.toMinutes(14))
            return;

        // check if data contains enough history and trend data for the tests to work
        ReadingData readingData = new ReadingData(rawTagData);
        if (readingData.getHistory().size() < numHistoryValues ||
                readingData.getTrend().size() != numTrendValues) {
            //Log.d("OpenLibre::Test", "history size: " + readingData.getHistory().size() + "  trend size: " + readingData.getTrend().size());
            return;
        }

        // add reading to realm, so when parsing the next reading, it can access this data
        realmProcessedData.beginTransaction();
        realmProcessedData.copyToRealmOrUpdate(readingData);
        realmProcessedData.commitTransaction();

        rawTagDataList.add(rawTagData);
        readingDataList.add(readingData);
    }

    @After
    public void tearDown() {
        realmRawData.close();
        // if any test opened the processed data realm, close it and delete the data
        if (realmProcessedData != null) {
            realmProcessedData.close();
            Realm.deleteRealm(realmConfigProcessedData);
        }
    }

    //@Ignore
    @Test
    public void testReparseAllData() {
        // delete all parsed readings before the tests
        Realm.deleteRealm(realmConfigProcessedData);

        // reparse all readings from raw data
        parseRawData();

        Realm realmProcessedData = Realm.getInstance(realmConfigProcessedData);
        assertThat(realmProcessedData.isEmpty(), is(false));
        realmProcessedData.close();
    }

    @Test
    public void testTrendIndexVsSensorAge() {
        parseTestData();
        ArrayList<Integer> results = new ArrayList<>();

        for (RawTagData rawTagData : rawTagDataList) {
            int diff = ((rawTagData.getSensorAgeInMinutes() % numTrendValues) - rawTagData.getIndexTrend() + numTrendValues) % numTrendValues;
            results.add(diff);
            if (diff > 1) Log.w("OpenLibre::TEST", "failed for: sensorAge: " + rawTagData.getSensorAgeInMinutes() + "  trendIndex: " + rawTagData.getIndexTrend());
        }
        Log.d("OpenLibre::TEST", "sensorAge % numTrendValues - trendIndex:  " + results);

        assertThat("trend index drifted away from sensor age more than one minute", results, everyItem(lessThanOrEqualTo(1)));
    }

    @Test
    public void testHistoryDatesMatchOnOverlappingReadings() {
        parseTestData();
        ArrayList<Integer> results = new ArrayList<>();

        ReadingData oldReadingData = readingDataList.get(0);
        results.add(0);
        for (ReadingData readingData : readingDataList.subList(1, readingDataList.size())) {
            GlucoseData oldGlucoseData = oldReadingData.getHistory().last();
            boolean found = false;
            for (GlucoseData glucoseData : readingData.getHistory()) {
                if (oldGlucoseData.glucose() == glucoseData.glucose()) {
                    if (oldGlucoseData.getAgeInSensorMinutes() - glucoseData.getAgeInSensorMinutes() == 0) {
                        // well matched
                        results.add(0);
                        found = true;
                        break;
                    } else if (abs(oldGlucoseData.getAgeInSensorMinutes() - glucoseData.getAgeInSensorMinutes()) < historyIntervalInMinutes) {
                        results.add(oldGlucoseData.getAgeInSensorMinutes() - glucoseData.getAgeInSensorMinutes());
                        found = true;
                        break;
                    }
                }
            }
            if (!found)
                results.add(0);
            oldReadingData = readingData;
        }
        Log.d("OpenLibre::TEST", "age diff:  " + results);

        assertThat("history dates on overlapping readings don't match", results, everyItem(equalTo(0)));
        assertThat("no overlapping readings found", results.size(), greaterThan(0));
    }

    @Test
    public void testHistoryDatesFitTrendData() {
        parseTestData();
        ArrayList<Integer> firstTrendResults = new ArrayList<>();
        ArrayList<Integer> lastTrendResults = new ArrayList<>();

        for (ReadingData readingData : readingDataList) {
            GlucoseData lastHistory = readingData.getHistory().get(readingData.getHistory().size() - 1);
            GlucoseData firstTrend = readingData.getTrend().get(0);
            GlucoseData lastTrend = readingData.getTrend().get(readingData.getTrend().size() - 1);

            long lastHistoryMinutes = TimeUnit.MILLISECONDS.toMinutes(lastHistory.getDate());
            long lastTrendMinutes = TimeUnit.MILLISECONDS.toMinutes(lastTrend.getDate());
            long firstTrendMinutes = TimeUnit.MILLISECONDS.toMinutes(firstTrend.getDate());

            firstTrendResults.add((int)(lastHistoryMinutes - firstTrendMinutes));
            lastTrendResults.add((int)(lastTrendMinutes - lastHistoryMinutes));
        }
        Log.d("OpenLibre::TEST", "last history - first trend date: " + firstTrendResults);
        Log.d("OpenLibre::TEST", "last trend - last history date:  " + lastTrendResults);

        assertThat("history ends more than 3 minutes before first trend", firstTrendResults, everyItem(greaterThanOrEqualTo(-3)));
        assertThat("history ends after last trend", lastTrendResults, everyItem(greaterThanOrEqualTo(0)));
    }

    @Test
    public void testHistoryValuesFitTrendData() {
        parseTestData();
        ArrayList<Float> minTrendResults = new ArrayList<>();
        ArrayList<Float> maxTrendResults = new ArrayList<>();

        for (ReadingData readingData : readingDataList) {
            GlucoseData lastHistory = readingData.getHistory().get(readingData.getHistory().size() - 1);
            ArrayList<Float> trendValues = new ArrayList<>();
            for (GlucoseData glucoseData : readingData.getTrend()) {
                trendValues.add(glucoseData.glucose(false));
            }

            minTrendResults.add(lastHistory.glucose(false) - Collections.min(trendValues));
            maxTrendResults.add(Collections.max(trendValues) - lastHistory.glucose(false));
        }
        Log.d("OpenLibre::TEST", "last history - min trend value: " + minTrendResults);
        Log.d("OpenLibre::TEST", "max trend value - last history: " + maxTrendResults);

        assertThat("last history value far smaller than trend values", minTrendResults, everyItem(greaterThanOrEqualTo(-10f)));
        assertThat("last history value far greater than trend values", maxTrendResults, everyItem(greaterThanOrEqualTo(-10f)));
    }

}