package com.rera.mss.measurement.computingBase

import com.rera.mss.exceptions.MonitoredAreaWithoutPvAreaException
import com.rera.mss.exceptions.PvAreaWithZeroAreaException
import com.rera.mss.exceptions.PvAreaWithoutPvPanelException
import com.rera.mss.model.*
import com.rera.mss.service.IrradianceAndEnergy
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.catchThrowable
import org.junit.jupiter.api.Test


class ComputerImplTest {

    @Test
    fun `given monitored area was defined with non zero pv area and pv panel the energy offset was empty list and got valid irradiance data when computeEnergyFromIrradiance then energy from irradiance will be computed`() {
        // given
        val monitoredArea = aMonitoredArea()
        monitoredArea.addPvPanelArea(aPvPanelArea(area = 10F, pvPanel = aPvPanel(efficiency = 10F)))
        val computer = ComputerImpl()

        // when
        val result = computer.computeEnergyFromIrradiance(
                monitoredArea, aTimeIrradianceData(), emptyList()
        )

        // then
        assertThat(result).containsExactlyElementsOf(
                listOf(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 2.0f, 4.5f, 8.0f, 12.5f, 18.0f, 24.5f, 31.5f, 38.0f, 44.0f, 49.5f, 53.5f, 56.0f, 57.5f, 58.0f, 58.0f, 58.0f, 58.0f, 58.0f)
        )
    }

    @Test
    fun `given energy offset was provided when computeEnergyFromIrradiance then it will be added to result energy`() {
        // given
        val monitoredArea = aMonitoredArea()
        monitoredArea.addPvPanelArea(aPvPanelArea(area = 10F, pvPanel = aPvPanel(efficiency = 10F)))
        val computer = ComputerImpl()

        // when
        val result = computer.computeEnergyFromIrradiance(
                monitoredArea, aTimeIrradianceData(), anEnergyOffset()
        )

        // then
        assertThat(result).containsExactlyElementsOf(
                listOf(0F, 0F, 0F, 0F, 0F, 1F, 5F, 10.5F, 18F, 27.5F, 50F, 52.5F, 66.5F, 79F, 91F, 101.5F, 108.5F, 113F, 115.5F, 116F, 116F, 116F, 116F, 116F)
        )
    }

    @Test
    fun `given monitored area contained more pv areas when computeEnergyFromIrradiance the energy will be summed accross all of them`() {
        // given
        val monitoredArea = aMonitoredArea()
        monitoredArea.addPvPanelArea(aPvPanelArea(area = 2.5F, pvPanel = aPvPanel(efficiency = 10F)))
        monitoredArea.addPvPanelArea(aPvPanelArea(area = 2.5F, pvPanel = aPvPanel(efficiency = 10F)))
        monitoredArea.addPvPanelArea(aPvPanelArea(area = 5F, pvPanel = aPvPanel(efficiency = 5F)))
        monitoredArea.addPvPanelArea(aPvPanelArea(area = 5F, pvPanel = aPvPanel(efficiency = 5F)))
        val computer = ComputerImpl()

        // when
        val result = computer.computeEnergyFromIrradiance(
                monitoredArea, aTimeIrradianceData(), anEnergyOffset()
        )

        // then
        assertThat(result).containsExactlyElementsOf(
                listOf(0F, 0F, 0F, 0F, 0F, 1F, 5F, 10.5F, 18F, 27.5F, 50F, 52.5F, 66.5F, 79F, 91F, 101.5F, 108.5F, 113F, 115.5F, 116F, 116F, 116F, 116F, 116F)
        )
    }


    @Test
    fun `given monitored area did not contain any pv areas when computeEnergyFromIrradiance will throw MonitoredAreaWithoutPvAreaException`() {
        // given
        val monitoredArea = aMonitoredArea()
        val computer = ComputerImpl()

        // when
        val thrown: Throwable = catchThrowable {
            computer.computeEnergyFromIrradiance(
                    monitoredArea, aTimeIrradianceData(), anEnergyOffset()
            )
        }

        // then
        assertThat(thrown).isInstanceOf(MonitoredAreaWithoutPvAreaException::class.java)
    }

    @Test
    fun `given monitored area did not contain any pv areas with pv panel defined when computeEnergyFromIrradiance will throw PvAreaWithoutPvPanelException`() {
        // given
        val monitoredArea = aMonitoredArea()
        monitoredArea.addPvPanelArea(aPvPanelArea(pvPanel = null))
        val computer = ComputerImpl()

        // when
        val thrown: Throwable = catchThrowable {
            computer.computeEnergyFromIrradiance(
                    monitoredArea, aTimeIrradianceData(), anEnergyOffset()
            )
        }

        // then
        assertThat(thrown).isInstanceOf(PvAreaWithoutPvPanelException::class.java)
    }

    @Test
    fun `given monitored area contained any pv areas with zero area when computeEnergyFromIrradiance will throw PvAreaWithZeroAreaException`() {
        // given
        val monitoredArea = aMonitoredArea()
        monitoredArea.addPvPanelArea(aPvPanelArea(area = 0F))
        val computer = ComputerImpl()

        // when
        val thrown: Throwable = catchThrowable {
            computer.computeEnergyFromIrradiance(
                    monitoredArea, aTimeIrradianceData(), anEnergyOffset()
            )
        }

        // then
        assertThat(thrown).isInstanceOf(PvAreaWithZeroAreaException::class.java)
    }

    @Test
    fun `given irradiance and energy data of one instrument were provided when computeInstrumentsEnergyWithUnifiedTimeBase will return same energy values`() {
        // given
        val data = anIrradianceAndEnergy(
                aTimeData(),
                anIrradianceData(),
                mapOf(aMonitoredArea() to aMonitoredAreaEnergy())
        )
        val instrumentPairedData = mapOf(anInstrument() to data)

        val step = 2 * 3600000L
        val computer = ComputerImpl()

        // when
        val result = computer.computeInstrumentsEnergyWithUnifiedTimeBase(instrumentPairedData, step)

        // then
        assertThat(result).containsExactlyElementsOf(
                listOf(
                        Pair(1579993200000, 0.0F),
                        Pair(1579996800000, 0.0F),
                        Pair(1580000400000, 0.0F),
                        Pair(1580004000000, 0.0F),
                        Pair(1580007600000, 0.0F),
                        Pair(1580011200000, 0.5F),
                        Pair(1580014800000, 2.0F),
                        Pair(1580018400000, 4.5F),
                        Pair(1580022000000, 8.0F),
                        Pair(1580025600000, 12.5F),
                        Pair(1580029200000, 18.0F),
                        Pair(1580032800000, 24.5F),
                        Pair(1580036400000, 31.5F),
                        Pair(1580040000000, 38.0F),
                        Pair(1580043600000, 44.0F),
                        Pair(1580047200000, 49.5F),
                        Pair(1580050800000, 53.5F),
                        Pair(1580054400000, 56.0F),
                        Pair(1580058000000, 57.5F),
                        Pair(1580061600000, 58.0F),
                        Pair(1580065200000, 58.0F),
                        Pair(1580068800000, 58.0F),
                        Pair(1580072400000, 58.0F),
                        Pair(1580076000000, 58.0F)
                )

        )
    }

    @Test
    fun `given data of two instruments with same time base were provided when computeInstrumentsEnergyWithUnifiedTimeBase will return sum of instrument energies`() {
        // given
        val data = anIrradianceAndEnergy(
                aTimeData(),
                anIrradianceData(),
                mapOf(aMonitoredArea() to aMonitoredAreaEnergy())
        )
        val instrumentPairedData = mapOf(anInstrument(id = 1) to data, anInstrument(id = 2) to data)
        val step = 2 * 3600000L
        val computer = ComputerImpl()

        // when
        val result = computer.computeInstrumentsEnergyWithUnifiedTimeBase(instrumentPairedData, step)
        val (unifiedTime, energy) = result.unzip()

        // then
        assertThat(energy).isEqualTo(listOf(0.0f, 0.0f, 2.4999995f, 12.499998f, 30.499998f, 55.999996f, 82.0f, 103.0f, 113.5f, 116.0f, 116.0f, 116.0f))
        assertThat(unifiedTime).isEqualTo(listOf(1579996800000, 1580004000000, 1580011200000, 1580018400000,
                1580025600000, 1580032800000, 1580040000000, 1580047200000,
                1580054400000, 1580061600000, 1580068800000, 1580076000000))

    }

    @Test
    fun `given data of two instruments with overlapping time bases were provided when computeInstrumentsEnergyWithUnifiedTimeBase will return sum of energies in overlapping interval`() {
        // given
        val dataInstrument1 = anIrradianceAndEnergy(
                listOf(1579993200000, 1579996800000, 1580000400000, 1580004000000, 1580007600000, 1580011200000, 1580014800000, 1580018400000,
                        1580022000000, 1580025600000, 1580029200000, 1580032800000, 1580036400000, 1580040000000, 1580043600000, 1580047200000), // this row overlaps
                emptyList(),
                mapOf(aMonitoredArea() to
                        listOf(
                                0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 2.0f, 4.5f, 8.0f, 12.5f, 18.0f, 24.5f, 31.5f, 38.0f, 44.0f, 49.5f
                        )
                )
        )

        val dataInstrument2 = anIrradianceAndEnergy(
                listOf(
                        1580022000000, 1580025600000, 1580029200000, 1580032800000, 1580036400000, 1580040000000, 1580043600000, 1580047200000, // this row overlaps
                        1580050800000, 1580054400000, 1580058000000, 1580061600000, 1580065200000, 1580068800000, 1580072400000, 1580076000000
                ),
                emptyList(),
                mapOf(aMonitoredArea() to
                        listOf(
                                8.0f, 12.5f, 18.0f, 24.5f, 31.5f, 38.0f, 44.0f, 49.5f, 53.5f, 56.0f, 57.5f, 58.0f, 58.0f, 58.0f, 58.0f, 58.0f
                        )
                )
        )

        val instrumentPairedData = mapOf(anInstrument(id = 1) to dataInstrument1, anInstrument(id = 2) to dataInstrument2)
        val step = 2 * 3600000L
        val computer = ComputerImpl()

        // when
        val result = computer.computeInstrumentsEnergyWithUnifiedTimeBase(instrumentPairedData, step)
        val (unifiedTime, energy) = result.unzip()

        // then
        assertThat(energy).isEqualTo(listOf(30.499998f, 55.999996f, 82.0f, 99.0f))
        assertThat(unifiedTime).isEqualTo(listOf(1580025600000, 1580032800000, 1580040000000, 1580047200000))
    }

    @Test
    fun `given time bases of two instruments did not overlap when computeInstrumentsEnergyWithUnifiedTimeBase will return empty list`() {
        // given
        val dataInstrument1 = anIrradianceAndEnergy(
                listOf(1579993200000, 1579996800000, 1580000400000, 1580004000000, 1580007600000, 1580011200000, 1580014800000, 1580018400000),
                emptyList(),
                mapOf(aMonitoredArea() to listOf(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 2.0f, 4.5f, 8.0f, 12.5f, 18.0f, 24.5f, 31.5f, 38.0f, 44.0f, 49.5f))
        )

        val dataInstrument2 = anIrradianceAndEnergy(
                listOf(1580050800000, 1580054400000, 1580058000000, 1580061600000, 1580065200000, 1580068800000, 1580072400000, 1580076000000),
                emptyList(),
                mapOf(aMonitoredArea() to listOf(8.0f, 12.5f, 18.0f, 24.5f, 31.5f, 38.0f, 44.0f, 49.5f, 53.5f, 56.0f, 57.5f, 58.0f, 58.0f, 58.0f, 58.0f, 58.0f))
        )

        val instrumentPairedData = mapOf(anInstrument(id = 1) to dataInstrument1, anInstrument(id = 2) to dataInstrument2)
        val step = 2 * 3600000L
        val computer = ComputerImpl()

        // when
        val result = computer.computeInstrumentsEnergyWithUnifiedTimeBase(instrumentPairedData, step)
        val energy = result.unzip().second

        // then
        assertThat(energy).isEqualTo(emptyList<Float>())
    }

    @Test
    fun `given time bases of two instruments differed in time step and the unified time step was set equal to bigger step when computeInstrumentsEnergyWithUnifiedTimeBase will return sum energies with unified time base`() {
        // given
        val oneHourStepInstrumentData = anIrradianceAndEnergy(
                listOf(1579993200000, 1579996800000, 1580000400000, 1580004000000, 1580007600000, 1580011200000, 1580014800000, 1580018400000,
                        1580022000000, 1580025600000, 1580029200000, 1580032800000, 1580036400000, 1580040000000, 1580043600000, 1580047200000), // this row overlaps
                emptyList(),
                mapOf(aMonitoredArea() to
                        listOf(
                                0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 2.0f, 4.5f,
                                8.0f, 12.5f, 18.0f, 24.5f, 31.5f, 38.0f, 44.0f, 49.5f  // this row overlaps
                        )
                )
        )

        val twoHoursStepInstrumentData = anIrradianceAndEnergy(
                listOf(
                        1580022000000, 1580029200000, 1580036400000, 1580043600000,  // this row overlaps
                        1580050800000, 1580058000000, 1580065200000, 1580072400000
                ),
                emptyList(),
                mapOf(aMonitoredArea() to
                        listOf(
                                8.0f, 18.0f, 31.5f, 44.0f, // this row overlaps
                                53.5f, 57.5f, 58.0f, 58.0f
                        )
                )
        )


        val instrumentPairedData = mapOf(anInstrument(id = 1) to oneHourStepInstrumentData, anInstrument(id = 2) to twoHoursStepInstrumentData)
        val step = 2 * 3600000L
        val computer = ComputerImpl()

        // when
        val result = computer.computeInstrumentsEnergyWithUnifiedTimeBase(instrumentPairedData, step)
        val (unifiedTime, energy) = result.unzip()

        // then
        assertThat(energy).isEqualTo(listOf(33.249996F, 59.499996F, 85.0F, 103.0F))
        assertThat(unifiedTime).isEqualTo(listOf(1580025600000, 1580032800000, 1580040000000, 1580047200000))
    }

    @Test
    fun `given time bases of two instruments differed in time step and the unified time step was bigger than any of individual steps when computeInstrumentsEnergyWithUnifiedTimeBase will return sum energies with unified time base`() {
        // given
        val oneHourStepInstrumentData = anIrradianceAndEnergy(
                listOf(1579993200000, 1579996800000, 1580000400000, 1580004000000, 1580007600000, 1580011200000, 1580014800000, 1580018400000,
                        1580022000000, 1580025600000, 1580029200000, 1580032800000, 1580036400000, 1580040000000, 1580043600000, 1580047200000), // this row overlaps
                emptyList(),
                mapOf(aMonitoredArea() to
                        listOf(
                                0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 2.0f, 4.5f,
                                8.0f, 12.5f, 18.0f, 24.5f, 31.5f, 38.0f, 44.0f, 49.5f  // this row overlaps
                        )
                )
        )

        val twoHoursStepInstrumentData = anIrradianceAndEnergy(
                listOf(
                        1580022000000, 1580029200000, 1580036400000, 1580043600000,  // this row overlaps
                        1580050800000, 1580058000000, 1580065200000, 1580072400000
                ),
                emptyList(),
                mapOf(aMonitoredArea() to
                        listOf(
                                8.0f, 18.0f, 31.5f, 44.0f, // this row overlaps
                                53.5f, 57.5f, 58.0f, 58.0f
                        )
                )
        )


        val instrumentPairedData = mapOf(anInstrument(id = 1) to oneHourStepInstrumentData, anInstrument(id = 2) to twoHoursStepInstrumentData)
        val step = 3 * 3600000L
        val computer = ComputerImpl()

        // when
        val result = computer.computeInstrumentsEnergyWithUnifiedTimeBase(instrumentPairedData, step)
        val (unifiedTime, energy) = result.unzip()

        // then
        assertThat(energy).isEqualTo(listOf(40.83333f, 77.666664f, 103.0f))
        assertThat(unifiedTime).isEqualTo(listOf(1580027400000, 1580038200000, 1580047200000))
    }

    @Test
    fun `given theoretical energy data have same time values as real energy data provided when computePerformanceRatio will correctly compute pr`(){
        // given
        val theoreticalEnergy = listOf(
                1L to 1F,
                3L to 6F
        )
        val realEnergy = listOf(
                1L to 1F,
                3L to 3F
        )
        val computer = ComputerImpl()

        // when
        val result = computer.computePerformanceRatio(theoreticalEnergy, realEnergy)
        val (time, pr) = result.unzip()

        // then
        assertThat(time).containsExactlyElementsOf(listOf(1L, 3L))
        assertThat(pr).containsExactlyElementsOf(listOf(100F, 50F))

    }

    @Test
    fun `given real energy data contain time values not contained in theoretical data provided when computePerformanceRatio will correctly compute pr using interpolation`(){
        // given
        val theoreticalEnergy = listOf(
                1L to 1F,
                5L to 5F,
                10L to 15F
        )
        val realEnergy = listOf(
                1L to 1F,
                2L to 1F,
                3L to 3F,
                7L to 4.5F,
                10L to 10F
        )
        val computer = ComputerImpl()

        // when
        val result = computer.computePerformanceRatio(theoreticalEnergy, realEnergy)
        val (time, pr) = result.unzip()

        // then
        assertThat(time).containsExactlyElementsOf(listOf(1L, 2L, 3L,7L, 10L))
        assertThat(pr).containsExactlyElementsOf(listOf(100F, 50F, 100F, 50F, 66.66667F))

    }

}

fun aMonitoredArea(id: Long = 0,
                   description: String = "",
                   user: User = aUser(),
                   instrument: Instrument? = anInstrument(),
                   inverter: Inverter? = anInverter()
) = MonitoredArea(id, description, user, instrument, inverter)

fun aPvPanelArea(
        id: Long = 0,
        area: Float = 0F,
        altitude: Float = 0F,
        tilt: Float = 0F,
        direction: Float = 0F,
        monitoredArea: MonitoredArea = aMonitoredArea(),
        pvPanel: PvPanel? = aPvPanel()
) = PvPanelArea(id, area, altitude, tilt, direction, monitoredArea, pvPanel)

fun aPvPanel(
        id: Long = 0,
        efficiency: Float = 0F,
        manufacturer: String = "",
        material: String = ""
) = PvPanel(id, efficiency, manufacturer, material)


fun aUser(id: Long = 0,
          username: String = "",
          email: String = "",
          password: String = "",
          firstName: String = "",
          lastName: String = ""
) = User(id, username, email, password, firstName, lastName)

fun anInstrument(id: Long = 0,
                 gpsCoordinates: String = "",
                 guid: String = "",
                 altitude: Float = 0F,
                 tilt: Float = 0F,
                 direction: Float = 0F,
                 calibrationValue: Float = 0F,
                 user: User = aUser()
) = Instrument(id, gpsCoordinates, guid, altitude, tilt, direction, calibrationValue, user)

fun anInverter(
        id: Long = 0,
        manufacturer: String = "",
        efficiency: Float = 0F,
        user: User = aUser()
) = Inverter(id, manufacturer, efficiency, user)

fun anIrradianceAndEnergy(
        time: List<Long> = emptyList(),
        irradiance: List<Float> = emptyList(),
        monitoredAreaEnergies: Map<MonitoredArea, List<Float>> = emptyMap()
) = IrradianceAndEnergy(time, irradiance, monitoredAreaEnergies)

fun aTimeData(): List<Long> = listOf(
        1579993200000, 1579996800000, 1580000400000, 1580004000000, 1580007600000, 1580011200000, 1580014800000, 1580018400000,
        1580022000000, 1580025600000, 1580029200000, 1580032800000, 1580036400000, 1580040000000, 1580043600000, 1580047200000,
        1580050800000, 1580054400000, 1580058000000, 1580061600000, 1580065200000, 1580068800000, 1580072400000, 1580076000000
)

fun anIrradianceData(): List<Float> = listOf(
        0.00F, 0.00F, 0.00F, 0.00F, 0.00F, 1F, 2F, 3F, 4F, 5F, 6F, 7F, 7F, 6F, 6F, 5F, 3F, 2F, 1F, 0.00F, 0.00F, 0.00F, 0.00F, 0.00F
)

fun aTimeIrradianceData(): List<Pair<Long, Float>> = aTimeData().zip(anIrradianceData())

fun anEnergyOffset(): List<Float> = listOf(0F, 0F, 0F, 0F, 0F, 0.5F, 3F, 6F, 10F, 15F, 32F, 28F, 35F, 41F, 47F, 52F, 55F, 57F, 58F, 58F, 58F, 58F, 58F, 58F)

fun aMonitoredAreaEnergy(): List<Float> = listOf(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 2.0f, 4.5f, 8.0f, 12.5f, 18.0f, 24.5f, 31.5f, 38.0f, 44.0f, 49.5f, 53.5f, 56.0f, 57.5f, 58.0f, 58.0f, 58.0f, 58.0f, 58.0f)