package net.pokeranalytics.android

import android.os.Looper
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.realm.RealmResults
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.ComputedResults
import net.pokeranalytics.android.calculus.SessionGroup
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.TimeFrame
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import java.text.SimpleDateFormat
import java.util.*

/**
 * Instrumented test, which will execute on an Android device.
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
@RunWith(AndroidJUnit4::class)

class ExampleInstrumentedUnitTest : RealmInstrumentedUnitTest() {

    // convenience extension
    fun Session.Companion.testInstance(netResult: Double, startDate: Date, endDate: Date?): Session {
        var session: Session = Session.newInstance()
        session.result?.netResult = netResult
        session.timeFrame?.setDate(startDate, endDate)
        return session
    }

    @Test
    fun testSessionStats() {

        val realm = this.mockRealm
        realm.beginTransaction()

        var s1 = realm.createObject(Session::class.java, "1")
        var s2 = realm.createObject(Session::class.java, "2")

        s1.timeFrame = realm.createObject(TimeFrame::class.java)
        s2.timeFrame = realm.createObject(TimeFrame::class.java)

        s1.result = realm.createObject(net.pokeranalytics.android.model.realm.Result::class.java)
        s2.result = realm.createObject(net.pokeranalytics.android.model.realm.Result::class.java)

        s1.result?.buyin = 100.0 // net result = -100
        s2.result?.buyin = 200.0
        s2.result?.cashout = 500.0 // net result = 300

        s1.cgBigBlind = 0.5 // bb net result = -200bb
        s2.cgBigBlind = 2.0 // bb net result = 150bb

        realm.insert(s1)
        realm.insert(s2)
        realm.commitTransaction()

        val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")

        val sd1 = sdf.parse("01/1/2019 10:00")
        val ed1 = sdf.parse("01/1/2019 11:00")
        val sd2 = sdf.parse("02/1/2019 08:00")
        val ed2 = sdf.parse("02/1/2019 11:00")

        realm.beginTransaction()

        s1.timeFrame?.setDate(sd1, ed1) // duration = 1h, hourly = -100, bb100 = -200bb / 25hands * 100 = -800
        s2.timeFrame?.setDate(sd2, ed2) // duration = 3h, hourly = 100, bb100 = 150 / 75 * 100 = +200

        realm.copyToRealmOrUpdate(s1)
        realm.copyToRealmOrUpdate(s2)

        realm.commitTransaction()

        val sessions = realm.where(Session::class.java).findAll()
        val group = SessionGroup(name = "test", sessions = sessions)

        var options = Calculator.Options()
        options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)

        val results: ComputedResults = Calculator.compute(group, options)
        val delta = 0.01

        val sum = results.computedStat(Stat.NETRESULT)
        if (sum != null) {
            assertEquals(200.0, sum.value, delta)
        } else {
            Assert.fail("No Net result stat")
        }

        val average = results.computedStat(Stat.AVERAGE)
        if (average != null) {
            assertEquals(100.0, average.value, delta)
        } else {
            Assert.fail("No AVERAGE stat")
        }

        val duration = results.computedStat(Stat.DURATION)
        if (duration != null) {
            assertEquals(4.0, duration.value, delta)
        } else {
            Assert.fail("No duration stat")
        }

        val hourlyRate = results.computedStat(Stat.HOURLY_RATE)
        if (hourlyRate != null) {
            assertEquals(50.0, hourlyRate.value, delta)
        } else {
            Assert.fail("No houry rate stat")
        }
        val handsPlayed = results.computedStat(Stat.HANDS_PLAYED)
        if (handsPlayed != null) {
            assertEquals(100.0, handsPlayed.value, delta)
        } else {
            Assert.fail("No hands played stat")
        }
        val numberOfGames = results.computedStat(Stat.NUMBER_OF_GAMES)
        if (numberOfGames != null) {
            assertEquals(2, numberOfGames.value.toInt())
        } else {
            Assert.fail("No numberOfGames stat")
        }
        val numberOfSets = results.computedStat(Stat.NUMBER_OF_SETS)
        if (numberOfSets != null) {
            assertEquals(2, numberOfSets.value.toInt())
        } else {
            Assert.fail("No numberOfSets stat")
        }
        val avgBuyin = results.computedStat(Stat.AVERAGE_BUYIN)
        if (avgBuyin != null) {
            assertEquals(150.0, avgBuyin.value, delta)
        } else {
            Assert.fail("No avgBuyin stat")
        }
        val avgDuration = results.computedStat(Stat.AVERAGE_DURATION)
        if (avgDuration != null) {
            assertEquals(2.0, avgDuration.value, delta)
        } else {
            Assert.fail("No avgDuration stat")
        }
        val roi = results.computedStat(Stat.ROI)
        if (roi != null) {
            assertEquals(200 / 300.0, roi.value, delta)
        } else {
            Assert.fail("No roi stat")
        }

        val avgBBNet = results.computedStat(Stat.AVERAGE_NET_BB)
        if (avgBBNet != null) {
            assertEquals(-25.0, avgBBNet.value, delta)
        } else {
            Assert.fail("No avgBBNet stat")
        }
        val bbHourlyRate = results.computedStat(Stat.HOURLY_RATE_BB)
        if (bbHourlyRate != null) {
            assertEquals(-12.5, bbHourlyRate.value, delta)
        } else {
            Assert.fail("No bbHourlyRate stat")
        }
        val netbbPer100Hands = results.computedStat(Stat.NET_BB_PER_100_HANDS)
        if (netbbPer100Hands != null) {
            assertEquals(-50.0, netbbPer100Hands.value, delta)
        } else {
            Assert.fail("No netbbPer100Hands stat")
        }

        val stdHourly = results.computedStat(Stat.STANDARD_DEVIATION_HOURLY)
        if (stdHourly != null) {
            assertEquals(141.42, stdHourly.value, delta)
        } else {
            Assert.fail("No stdHourly stat")
        }

        val std = results.computedStat(Stat.STANDARD_DEVIATION)
        if (std != null) {
            assertEquals(282.84, std.value, delta)
        } else {
            Assert.fail("No std stat")
        }

        val std100 = results.computedStat(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS)
        if (std100 != null) {
            assertEquals(707.1, std100.value, delta)
        } else {
            Assert.fail("No std100 stat")
        }

    }

    @Test
    fun testOverlappingSessions1() {

        val realm = this.mockRealm
        realm.beginTransaction()

        var s1 = realm.createObject(Session::class.java, "1")
        var s2 = realm.createObject(Session::class.java, "2")

        s1.timeFrame = realm.createObject(TimeFrame::class.java)
        s2.timeFrame = realm.createObject(TimeFrame::class.java)

        s1.result = realm.createObject(net.pokeranalytics.android.model.realm.Result::class.java)
        s2.result = realm.createObject(net.pokeranalytics.android.model.realm.Result::class.java)

        realm.insert(s1)
        realm.insert(s2)
        realm.commitTransaction()

        val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")

        val sd1 = sdf.parse("01/1/2019 09:00")
        val ed1 = sdf.parse("01/1/2019 10:00")
        val sd2 = sdf.parse("01/1/2019 08:00")
        val ed2 = sdf.parse("01/1/2019 11:00")

        realm.beginTransaction()

        s1.timeFrame?.setDate(sd1, ed1) // duration = 1h, hourly = -100, bb100 = -200bb / 25hands * 100 = -800
        s2.timeFrame?.setDate(sd2, ed2) // duration = 4h, hourly = 100, bb100 = 150 / 75 * 100 = +200

        realm.copyToRealmOrUpdate(s1)
        realm.copyToRealmOrUpdate(s2)

        realm.commitTransaction()

        val sessions = realm.where(Session::class.java).findAll()
        val group = SessionGroup(name = "test", sessions = sessions)

        var options = Calculator.Options()
        options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)

        val results: ComputedResults = Calculator.compute(group, options)
        val delta = 0.01

        val duration = results.computedStat(Stat.DURATION)
        if (duration != null) {
            assertEquals(3.0, duration.value, delta)
        } else {
            Assert.fail("No Net result stat")
        }

        val numberOfSets = results.computedStat(Stat.NUMBER_OF_SETS)
        if (numberOfSets != null) {
            assertEquals(1, numberOfSets.value.toInt())
        } else {
            Assert.fail("No numberOfSets stat")
        }

        val numberOfGames = results.computedStat(Stat.NUMBER_OF_GAMES)
        if (numberOfGames != null) {
            assertEquals(2, numberOfGames.value.toInt())
        } else {
            Assert.fail("No numberOfSets stat")
        }
    }

    @Test
    fun testOverlappingSessions2() {

        val realm = this.mockRealm
        realm.beginTransaction()

        var s1 = realm.createObject(Session::class.java, "1")
        var s2 = realm.createObject(Session::class.java, "2")
        var s3 = realm.createObject(Session::class.java, "3")

        s1.timeFrame = realm.createObject(TimeFrame::class.java)
        s2.timeFrame = realm.createObject(TimeFrame::class.java)
        s3.timeFrame = realm.createObject(TimeFrame::class.java)

        realm.insert(s1)
        realm.insert(s2)
        realm.insert(s3)
        realm.commitTransaction()

        val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")

        val sd1 = sdf.parse("01/1/2019 05:00")
        val ed1 = sdf.parse("01/1/2019 09:00")
        val sd2 = sdf.parse("01/1/2019 07:00")
        val ed2 = sdf.parse("01/1/2019 11:00")
        val sd3 = sdf.parse("01/1/2019 03:00")
        val ed3 = sdf.parse("01/1/2019 06:00")

        realm.beginTransaction()

        s1.timeFrame?.setDate(sd1, ed1) // duration = 4h
        s2.timeFrame?.setDate(sd2, ed2) // duration = 4h
        s3.timeFrame?.setDate(sd3, ed3) // duration = 3h

        realm.copyToRealmOrUpdate(s1)
        realm.copyToRealmOrUpdate(s2)
        realm.copyToRealmOrUpdate(s3)

        realm.commitTransaction()

        val sessions = realm.where(Session::class.java).findAll()
        val group = SessionGroup(name = "test", sessions = sessions)

        var options = Calculator.Options()
        options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)

        val results: ComputedResults = Calculator.compute(group, options)
        val delta = 0.01

        val duration = results.computedStat(Stat.DURATION)
        if (duration != null) {
            assertEquals(8.0, duration.value, delta)
        } else {
            Assert.fail("No Net result stat")
        }

        val numberOfSets = results.computedStat(Stat.NUMBER_OF_SETS)
        if (numberOfSets != null) {
            assertEquals(1, numberOfSets.value.toInt())
        } else {
            Assert.fail("No numberOfSets stat")
        }

        val numberOfGames = results.computedStat(Stat.NUMBER_OF_GAMES)
        if (numberOfGames != null) {
            assertEquals(3, numberOfGames.value.toInt())
        } else {
            Assert.fail("No numberOfSets stat")
        }

    }

    var sessions: RealmResults<Session>? = null

    @Test
    fun testOverlappingSessionDeletion() {

        val realm = this.mockRealm
        this.sessions = realm.where(Session::class.java).findAll() // monitor session deletions

        Looper.prepare()
        this.sessions?.addChangeListener { t, changeSet ->

            val deletedSessions = realm.where(Session::class.java).`in`("id", changeSet.deletions.toTypedArray()).findAll()
            deletedSessions.forEach { it.cleanup() }

        }
        Looper.loop()

        realm.beginTransaction()

        var s1 = realm.createObject(Session::class.java, "1")
        var s2 = realm.createObject(Session::class.java, "2")
        var s3 = realm.createObject(Session::class.java, "3")

        s1.timeFrame = realm.createObject(TimeFrame::class.java)
        s2.timeFrame = realm.createObject(TimeFrame::class.java)
        s3.timeFrame = realm.createObject(TimeFrame::class.java)

        realm.insert(s1)
        realm.insert(s2)
        realm.insert(s3)
        realm.commitTransaction()

        val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")

        val sd1 = sdf.parse("01/1/2019 05:00")
        val ed1 = sdf.parse("01/1/2019 09:00")
        val sd2 = sdf.parse("01/1/2019 07:00")
        val ed2 = sdf.parse("01/1/2019 11:00")
        val sd3 = sdf.parse("01/1/2019 03:00")
        val ed3 = sdf.parse("01/1/2019 06:00")

        realm.beginTransaction()

        s1.timeFrame?.setDate(sd1, ed1) // duration = 4h
        s2.timeFrame?.setDate(sd2, ed2) // duration = 4h
        s3.timeFrame?.setDate(sd3, ed3) // duration = 3h

        realm.copyToRealmOrUpdate(s1)
        realm.copyToRealmOrUpdate(s2)
        realm.copyToRealmOrUpdate(s3)

        realm.commitTransaction()

        val sessions = realm.where(Session::class.java).findAll()
        val group = SessionGroup(name = "test", sessions = sessions)

        var options = Calculator.Options()
        options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)

        val results: ComputedResults = Calculator.compute(group, options)
        val delta = 0.01

        val duration = results.computedStat(Stat.DURATION)
        if (duration != null) {
            assertEquals(8.0, duration.value, delta)
        } else {
            Assert.fail("No duration stat")
        }

        realm.beginTransaction()
        s1.deleteFromRealm()
        realm.commitTransaction()
//        realm.executeTransaction {
//            s1.deleteFromRealm()
//        }

        val group2 = SessionGroup(name = "test", sessions = sessions)
        val results2: ComputedResults = Calculator.compute(group2, options)

        val duration2 = results2.computedStat(Stat.DURATION)
        if (duration2 != null) {
            assertEquals(7.0, duration2.value, delta)
        } else {
            Assert.fail("No duration2 stat")
        }

    }
}