package cz.senslog.analytics.utils;

import cz.senslog.analytics.domain.*;
import cz.senslog.analytics.domain.ComparisonOperator;
import cz.senslog.analytics.utils.validator.NotifyTrigger;
import cz.senslog.analytics.utils.validator.ThresholdChecker;
import cz.senslog.analytics.utils.validator.Validator;
import cz.senslog.analytics.utils.validator.domain.ValidationResult;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;

import static cz.senslog.analytics.domain.AttributeType.TIME;
import static cz.senslog.analytics.domain.AttributeType.VAL;
import static cz.senslog.analytics.domain.ComparisonOperator.*;
import static cz.senslog.analytics.utils.validator.NotifyTrigger.Mode.INSTANT;
import static cz.senslog.analytics.utils.validator.NotifyTrigger.Mode.ON_CHANGE;
import static org.junit.jupiter.api.Assertions.*;

class ThresholdCheckerTest {

    private static final Sensor SENSOR = new Sensor(100L, 1000L, 1001L);

    private static Validator<Observation> validator;

    @BeforeAll
    static void setUp() {
        validator = Validator.<Observation>create()
                .addMapping(VAL, o -> o::value)
                .addMapping(AttributeType.TIME, o -> () ->  (double) o.timestamp().toLocalTime().toSecondOfDay());
    }

    @Test
    void test_1() {

        final OffsetDateTime startInterval = OffsetDateTime.of(2024, 1, 1, 12, 0, 0, 0, ZoneOffset.ofHours(2));
        final OffsetDateTime endInterval = OffsetDateTime.of(2024, 1, 1, 15, 0, 0, 0, ZoneOffset.ofHours(2));


        ThresholdChecker<Observation> checker = ThresholdChecker.create(
                List.of(
                        Threshold.of(1, SENSOR.id(), true, false, true, null, List.of(
                                ThresholdDimension.of(1, INSTANT, VAL, List.of(
                                            ThresholdDimensionRule.of(GT, 25)
                                        )
                                ),
                                ThresholdDimension.of(2, INSTANT, TIME, List.of(
                                            ThresholdDimensionRule.of(GE, 43200),
                                            ThresholdDimensionRule.of(LE, 54000)
                                        )
                                )
                        ))
                ),
                validator,
                (alert) -> {
                    assertEquals(SENSOR.id(), alert.datasourceId());
                    assertFalse(alert.timestamp().isBefore(startInterval) || alert.timestamp().isAfter(endInterval));
                    assertEquals(2, alert.violatedData().length);
                },
                true
        );

        List<Observation> passedObservations = Stream.of(
        /* pass | none  */ Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 25.3, OffsetDateTime.of(2024, 1, 1, 11, 59, 59, 0, ZoneOffset.ofHours(2)))),
        /* fail | alert */ Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 25.3, OffsetDateTime.of(2024, 1, 1, 12, 0, 0, 0, ZoneOffset.ofHours(2)))),
        /* fail | alert */ Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 25.3, OffsetDateTime.of(2024, 1, 1, 12, 0, 1, 0, ZoneOffset.ofHours(2)))),
        /* fail | alert */ Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 25.3, OffsetDateTime.of(2024, 1, 1, 14, 59, 59, 0, ZoneOffset.ofHours(2)))),
        /* fail | alert */ Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 25.3, OffsetDateTime.of(2024, 1, 1, 15, 0, 0, 0, ZoneOffset.ofHours(2)))),
        /* pass | none  */ Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 25.3, OffsetDateTime.of(2024, 1, 1, 15, 0, 1, 0, ZoneOffset.ofHours(2))))
        ).filter(checker::check).toList();

        assertEquals(2, passedObservations.size());
    }

    @Test
    void test_2() {

        final OffsetDateTime startInterval = OffsetDateTime.of(2024, 1, 1, 12, 0, 0, 0, ZoneOffset.ofHours(2));
        final OffsetDateTime endInterval = OffsetDateTime.of(2024, 1, 1, 15, 0, 0, 0, ZoneOffset.ofHours(2));


        ThresholdChecker<Observation> checker = ThresholdChecker.create(
                List.of(
                        Threshold.of(1, SENSOR.id(), true, false, true, null, List.of(
                                ThresholdDimension.of(1, ON_CHANGE, VAL, List.of(
                                                ThresholdDimensionRule.of(GT, 25)
                                        )
                                ),
                                ThresholdDimension.of(2, INSTANT, TIME, List.of(
                                                ThresholdDimensionRule.of(GE, 43200),
                                                ThresholdDimensionRule.of(LE, 54000)
                                        )
                                )
                        )),
                        Threshold.of(2, SENSOR.id(), true, false, true, null, List.of(
                                ThresholdDimension.of(3, INSTANT, VAL, List.of(
                                                ThresholdDimensionRule.of(GT, 25)
                                        )
                                ),
                                ThresholdDimension.of(4, ON_CHANGE, TIME, List.of(
                                                ThresholdDimensionRule.of(GE, 43200),
                                                ThresholdDimensionRule.of(LE, 54000)
                                        )
                                )
                        ))
                ),
                validator,
                (alert) -> {
                    assertEquals(SENSOR.id(), alert.datasourceId());
                    assertTrue(alert.timestamp().isEqual(startInterval.plusSeconds(1)) || alert.timestamp().isEqual(endInterval));
                    assertEquals(2, alert.violatedData().length);
                },
                true
        );

        List<Observation> passedObservations = Stream.of(
        /* pass | none  */ Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 24.3, OffsetDateTime.of(2024, 1, 1, 11, 59, 59, 0, ZoneOffset.ofHours(2)))),
        /* pass | none  */ Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 24.3, OffsetDateTime.of(2024, 1, 1, 12, 0, 0, 0, ZoneOffset.ofHours(2)))),
        /* fail | alert */ Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 25.3, OffsetDateTime.of(2024, 1, 1, 12, 0, 1, 0, ZoneOffset.ofHours(2)))),
        /* fail | none  */ Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 25.3, OffsetDateTime.of(2024, 1, 1, 14, 59, 59, 0, ZoneOffset.ofHours(2)))),
        /* pass | alert */ Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 24.3, OffsetDateTime.of(2024, 1, 1, 15, 0, 0, 0, ZoneOffset.ofHours(2)))),
        /* pass | none  */ Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 25.3, OffsetDateTime.of(2024, 1, 1, 15, 0, 1, 0, ZoneOffset.ofHours(2))))
        ).filter(checker::check).toList();

        assertEquals(4, passedObservations.size());
    }

    @Test
    void test_3() {

        final OffsetDateTime startInterval = OffsetDateTime.of(2024, 1, 1, 12, 0, 0, 0, ZoneOffset.ofHours(2));
        final OffsetDateTime endInterval = OffsetDateTime.of(2024, 1, 1, 15, 0, 0, 0, ZoneOffset.ofHours(2));


        ThresholdChecker<Observation> checker = ThresholdChecker.create(
                List.of(
                        Threshold.of(1, SENSOR.id(), true, false, true, null, List.of(
                                ThresholdDimension.of(1, ON_CHANGE, VAL, List.of(
                                                ThresholdDimensionRule.of(GT, 25)
                                        )
                                ),
                                ThresholdDimension.of(2, INSTANT, TIME, List.of(
                                                ThresholdDimensionRule.of(GE, 43200),
                                                ThresholdDimensionRule.of(LE, 54000)
                                        )
                                )
                        )),
                        Threshold.of(2, SENSOR.id(), true, false, true, null, List.of(
                                ThresholdDimension.of(3, INSTANT, VAL, List.of(
                                                ThresholdDimensionRule.of(GT, 25)
                                        )
                                ),
                                ThresholdDimension.of(4, ON_CHANGE, TIME, List.of(
                                                ThresholdDimensionRule.of(GE, 43200),
                                                ThresholdDimensionRule.of(LE, 54000)
                                        )
                                )
                        ))
                ),
                validator,
                (alert) -> {
                    assertEquals(SENSOR.id(), alert.datasourceId());
                    assertTrue(alert.timestamp().isEqual(startInterval) || alert.timestamp().isEqual(endInterval));
                    assertEquals(2, alert.violatedData().length);
                },
                true
        );

        List<Observation> passedObservations = Stream.of(
                /* pass | none  */ Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 25.3, OffsetDateTime.of(2024, 1, 1, 11, 59, 59, 0, ZoneOffset.ofHours(2)))),
                /* fail | alert */ Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 25.3, OffsetDateTime.of(2024, 1, 1, 12, 0, 0, 0, ZoneOffset.ofHours(2)))),
                /* fail | none  */ Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 25.3, OffsetDateTime.of(2024, 1, 1, 12, 0, 1, 0, ZoneOffset.ofHours(2)))),
                /* fail | none  */ Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 25.3, OffsetDateTime.of(2024, 1, 1, 14, 59, 59, 0, ZoneOffset.ofHours(2)))),
                /* pass | alert */ Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 24.3, OffsetDateTime.of(2024, 1, 1, 15, 0, 0, 0, ZoneOffset.ofHours(2)))),
                /* pass | none  */ Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 24.3, OffsetDateTime.of(2024, 1, 1, 15, 0, 1, 0, ZoneOffset.ofHours(2))))
        ).filter(checker::check).toList();

        assertEquals(3, passedObservations.size());
    }

    @Test
    void check_size_1_INSTANT() {

        ThresholdChecker<Observation> checker = new ThresholdChecker<>(
                List.of(
                    new Threshold(1, SENSOR.id(), true, false, true, null, List.of(
                            ThresholdDimension.of(1, INSTANT, VAL, List.of(ThresholdDimensionRule.of(GT, 10.0)))
                    ))
                ),
                Validator.<Observation>create()
                        .addMapping(VAL, o -> o::value),
                (report) -> {
                    assertEquals(SENSOR.id(), report.datasourceId());
                    assertEquals(1, report.violatedData().length);
                    ValidationResult valResult = report.violatedData()[0];
                    assertEquals(VAL, valResult.attributeType());
                    assertEquals(23.3, valResult.validatedValue());
                    assertEquals(1, valResult.records().size());
                },
                true
        );

        long count = Stream.of(
                Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 23.3, OffsetDateTime.now()))
        ).filter(checker::check).count();
        assertEquals(1, count);
    }

    @Test
    void check_size_3_ON_CHANGE() {

        ThresholdChecker<Observation> checker = new ThresholdChecker<>(
                List.of(
                        new Threshold(1, SENSOR.id(), true,false, true, null, List.of(
                                ThresholdDimension.of(1, NotifyTrigger.Mode.ON_CHANGE, VAL, List.of(ThresholdDimensionRule.of(GT, 10.0)))
                        ))
                ),
                Validator.<Observation>create()
                        .addMapping(VAL, o -> o::value),
                (report) -> {
                    assertEquals(SENSOR.id(), report.datasourceId());
                    assertEquals(1, report.violatedData().length);
                    ValidationResult valResult = report.violatedData()[0];
                    assertEquals(VAL, valResult.attributeType());
                    assertEquals(25.3, valResult.validatedValue());
                    assertEquals(1, valResult.records().size());
                },
                true
        );

        long count = Stream.of(
                Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(),9.9, OffsetDateTime.now())),
                Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(),25.3, OffsetDateTime.now())),
                Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(),30.5, OffsetDateTime.now()))
        ).filter(checker::check).count();
        assertEquals(3, count);
    }

    @Test
    void check_size_4_ON_CHANGE() {
        AtomicInteger reportStep = new AtomicInteger(1);
        ThresholdChecker<Observation> checker = new ThresholdChecker<>(
                List.of(
                        new Threshold(1, SENSOR.id(), true,false, true, null, List.of(
                                ThresholdDimension.of(1, NotifyTrigger.Mode.ON_CHANGE, VAL, List.of(ThresholdDimensionRule.of(GT, 10.0)))
                        ))
                ),
                Validator.<Observation>create()
                        .addMapping(VAL, o -> o::value),
                (report) -> {
                    assertEquals(SENSOR.id(), report.datasourceId());
                    assertEquals(1, report.violatedData().length);
                    ValidationResult valResult = report.violatedData()[0];
                    assertEquals(VAL, valResult.attributeType());

                    if (reportStep.compareAndSet(1, 2)) {
                        assertEquals(25.3, valResult.validatedValue());
                        assertTrue(valResult.isNotValid());
                        assertEquals(1, valResult.records().size());

                    } else if (reportStep.compareAndSet(2, 3)) {
                        assertEquals(8.5, valResult.validatedValue());
                        assertTrue(valResult.isValid());
                        assertEquals(0, valResult.records().size());
                    }
                },
                true
        );

        long count = Stream.of(
                Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 9.9, OffsetDateTime.now())),
                Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 25.3, OffsetDateTime.now())),
                Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(),30.5, OffsetDateTime.now())),
                Observation.of(SENSOR, RawObservation.of(SENSOR.unitId(), SENSOR.sensorId(), 8.5, OffsetDateTime.now()))
        ).filter(checker::check).count();
        assertEquals(4, count);
    }
    
    @Test
    void check_doubleStatistics_INSTANT() {
        long datasourceId = SENSOR.id();

        ThresholdChecker<DoubleStatistics> checker = new ThresholdChecker<>(
                List.of(
                        new Threshold(1, -1, true, false, true, null, List.of(
                                ThresholdDimension.of(1, INSTANT, AttributeType.MIN, List.of(ThresholdDimensionRule.of( ComparisonOperator.LT, 0.0))))
                        ),
                        new Threshold(1, -1, true, false, true, null, List.of(
                                ThresholdDimension.of(2, INSTANT, AttributeType.MAX, List.of(ThresholdDimensionRule.of(GT, 5.0))))
                        )
                ),
                Validator.<DoubleStatistics>create()
                        .addMapping(AttributeType.MIN, s -> s::min)
                        .addMapping(AttributeType.MAX, s -> s::max)
                        .addMapping(AttributeType.AVG, s -> s::average),
                (report) -> {
                    assertEquals(datasourceId, report.datasourceId());
                    assertEquals(2, report.violatedData().length);
                    ValidationResult[] violatedData = report.violatedData();
                    ValidationResult minResult = violatedData[0].attributeType().equals(AttributeType.MIN) ? violatedData[0] : violatedData[1];
                    ValidationResult maxResult = violatedData[0].attributeType().equals(AttributeType.MAX) ? violatedData[0] : violatedData[1];

                    assertEquals(AttributeType.MIN, minResult.attributeType());
                    assertEquals(-1.5, minResult.validatedValue());
                    assertEquals(1, minResult.records().size());

                    assertEquals(AttributeType.MAX, maxResult.attributeType());
                    assertEquals(5.2, maxResult.validatedValue());
                    assertEquals(1, maxResult.records().size());
                },
                true
        );

        DoubleStatistics ds = DoubleStatistics.init(datasourceId, OffsetDateTime.now());
        ds.accept(datasourceId, -1.5);
        ds.accept(datasourceId, 3.3);
        ds.accept(datasourceId, 5.2);


        long count = Stream.of(ds)
                .filter(checker::check).count();
        assertEquals(1, count);
    }
}