package de.dennisguse.opentracks.stats;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;

import androidx.test.ext.junit.runners.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import java.time.Duration;
import java.time.Instant;
import java.util.List;

import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;

@RunWith(AndroidJUnit4.class)
public class TrackStatisticsUpdaterTest {

    @Test
    public void empty() {
        // when
        TrackStatisticsUpdater subject = new TrackStatisticsUpdater();

        // then
        TrackStatistics statistics = subject.getTrackStatistics();
        assertNull(statistics.getStartTime());
        assertNull(statistics.getStopTime());
        assertEquals(Duration.ZERO, statistics.getTotalTime());
        assertEquals(Duration.ZERO, statistics.getMovingTime());

        assertEquals(Speed.of(0), statistics.getAverageSpeed());
        assertEquals(Speed.of(0), statistics.getAverageMovingSpeed());
        assertEquals(Speed.of(0), statistics.getMaxSpeed());

        assertNull(statistics.getTotalAltitudeGain());
        assertNull(statistics.getTotalAltitudeLoss());
        assertNull(statistics.getAverageHeartRate());
    }

    @Test
    public void startTime() {
        // given
        Instant startTime = Instant.parse("2021-10-24T23:00:00.000Z");
        TrackPoint tp = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, startTime);

        // when
        TrackStatisticsUpdater subject = new TrackStatisticsUpdater();

        subject.addTrackPoint(tp);

        // then
        TrackStatistics statistics = subject.getTrackStatistics();
        assertEquals(startTime, statistics.getStartTime());
        assertEquals(startTime, statistics.getStopTime());
        assertEquals(Duration.ZERO, statistics.getTotalTime());
        assertEquals(Duration.ZERO, statistics.getMovingTime());

        assertEquals(Speed.of(0), statistics.getAverageSpeed());
        assertEquals(Speed.of(0), statistics.getAverageMovingSpeed());
        assertEquals(Speed.of(0), statistics.getMaxSpeed());

        assertNull(statistics.getTotalAltitudeGain());
        assertNull(statistics.getTotalAltitudeLoss());
        assertNull(statistics.getAverageHeartRate());
    }

    @Test
    public void addTrackPoint_TestingTrack() {
        // given
        TestDataUtil.TrackData data = TestDataUtil.createTestingTrack(new Track.Id(1));

        // when
        TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
        data.trackPoints().forEach(subject::addTrackPoint);

        // then
        TrackStatistics statistics = subject.getTrackStatistics();
        assertEquals(128.03, statistics.getTotalDistance().toM(), 0.01);
        assertEquals(Duration.ofSeconds(13), statistics.getTotalTime());
        assertEquals(Duration.ofSeconds(9), statistics.getMovingTime());

        assertEquals(2.5, statistics.getMinAltitude(), 0.01);
        assertEquals(32.5, statistics.getMaxAltitude(), 0.01);
        assertEquals(36, statistics.getTotalAltitudeGain(), 0.01);
        assertEquals(36, statistics.getTotalAltitudeLoss(), 0.01);

        assertEquals(14.226, statistics.getMaxSpeed().toMPS(), 0.01);
        assertEquals(14.226, statistics.getAverageMovingSpeed().toMPS(), 0.01);
        assertEquals(9.84, statistics.getAverageSpeed().toMPS(), 0.01);
        assertEquals(106.85f, statistics.getAverageHeartRate().getBPM(), 0.01);
    }

    @Test
    public void addTrackPoint_distance_from_GPS_not_moving() {
        // given
        TrackStatisticsUpdater subject = new TrackStatisticsUpdater();

        TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
        TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
        TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));

        // when
        subject.addTrackPoint(tp1);
        subject.addTrackPoint(tp2);
        subject.addTrackPoint(tp3);

        // then
        assertEquals(0, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
    }

    @Test
    public void addTrackPoint_distance_from_GPS_moving() {
        // given
        TrackStatisticsUpdater subject = new TrackStatisticsUpdater();

        TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
        TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
        TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
        tp3.setSpeed(Speed.of(5f));

        // when
        subject.addTrackPoint(tp1);
        subject.addTrackPoint(tp2);
        subject.addTrackPoint(tp3);

        // then
        assertEquals(1.10, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
    }

    @Test
    public void addTrackPoint_distance_from_GPS_moving_and_sensor_moving() {
        // given
        TrackStatisticsUpdater subject = new TrackStatisticsUpdater();

        TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
        TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
        tp2.setSpeed(Speed.of(5f));
        TrackPoint tp3 = new TrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
        tp3.setSpeed(Speed.of(5f));
        TrackPoint tp4 = new TrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
        tp4.setSpeed(Speed.of(5f));
        tp4.setSensorDistance(Distance.of(5f));
        TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
        tp5.setSensorDistance(Distance.of(10f));

        // when
        subject.addTrackPoint(tp1);
        subject.addTrackPoint(tp2);
        subject.addTrackPoint(tp3);

        // then
        assertEquals(110.57, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);

        // when
        subject.addTrackPoint(tp4);
        subject.addTrackPoint(tp5);

        // then
        assertEquals(125.57, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
    }

    @Test
    public void addTrackPoint_distance_from_GPS_not_moving_and_sensor_moving() {
        // given
        TrackStatisticsUpdater subject = new TrackStatisticsUpdater();

        TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
        TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
        TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
        TrackPoint tp4 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
        tp4.setSensorDistance(Distance.of(5f));
        TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
        tp5.setSensorDistance(Distance.of(10f));

        // when
        subject.addTrackPoint(tp1);
        subject.addTrackPoint(tp2);
        subject.addTrackPoint(tp3);

        // then
        assertEquals(0, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);

        // when
        subject.addTrackPoint(tp4);
        subject.addTrackPoint(tp5);

        // then
        assertEquals(15, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
    }

    @Test
    public void addTrackPoint_distance_from_GPS_moving_and_sensor_disconnecting() {
        // given
        TrackStatisticsUpdater subject = new TrackStatisticsUpdater();

        TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
        TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
        tp2.setSpeed(Speed.of(5f));
        TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
        tp3.setSensorDistance(Distance.of(5f));
        tp3.setSpeed(Speed.of(5f));
        TrackPoint tp4 = new TrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
        tp4.setSpeed(Speed.of(5f));
        TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));

        // when
        subject.addTrackPoint(tp1);
        subject.addTrackPoint(tp2);
        subject.addTrackPoint(tp3);

        // then
        assertEquals(5, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);

        // when
        subject.addTrackPoint(tp4);
        subject.addTrackPoint(tp5);

        // then
        assertEquals(59.18, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
    }


    @Test
    public void addTrackPoint_maxSpeed_ignore_above_acceleration() {
        TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
        assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());

        subject.addTrackPoint(new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)));
        assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());

        // Ignore as we set max speed if two consecutive trackpoints were considered moving
        subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
                .setSpeed(Speed.of(1f)));
        assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());

        // Update max speed
        subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
                .setSpeed(Speed.of(1f)));
        assertEquals(Speed.of(1f), subject.getTrackStatistics().getMaxSpeed());

        // Update max speed
        subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(12))
                .setSpeed(Speed.of(50f)));
        assertEquals(Speed.of(50f), subject.getTrackStatistics().getMaxSpeed());

        // Ignore; we were getting slower
        subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(13))
                .setSpeed(Speed.of(5f)));
        assertEquals(Speed.of(50f), subject.getTrackStatistics().getMaxSpeed());

        // Ignore acceleration above 2g
        subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(14))
                .setSpeed(Speed.of(500f)));
        assertEquals(Speed.of(50f), subject.getTrackStatistics().getMaxSpeed());

    }

    @Test
    public void addTrackPoint_maxSpeed_multiple_segments() {
        TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
        assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());

        subject.addTrackPoints(List.of(
                new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)),
                new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
                        .setSpeed(Speed.of(2f)),
                new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
                        .setSpeed(Speed.of(2f)),
                new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(4))
        ));
        assertEquals(Speed.of(2f), subject.getTrackStatistics().getMaxSpeed());

        // when
        subject.addTrackPoints(List.of(
                new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(5)),
                new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(6))
                        .setSpeed(Speed.of(1f)),
                new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(7))
                        .setSpeed(Speed.of(1f)),
                new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(8))
        ));

        // then
        assertEquals(Speed.of(2f), subject.getTrackStatistics().getMaxSpeed());
    }

    @Test
    public void copy_constructor() {
        // given
        TrackStatisticsUpdater subject = new TrackStatisticsUpdater();

        TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
        TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
        tp2.setSpeed(Speed.of(5f));
        TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
        tp3.setSpeed(Speed.of(5f));
        TrackPoint tp4 = new TrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
        tp4.setSpeed(Speed.of(5f));
        TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));

        subject.addTrackPoint(tp1);
        subject.addTrackPoint(tp2);
        subject.addTrackPoint(tp3);
        subject.addTrackPoint(tp4);

        // when
        TrackStatisticsUpdater copy = new TrackStatisticsUpdater(subject);
        subject.addTrackPoint(tp5);
        copy.addTrackPoint(tp5);


        // then
        assertEquals(55.287, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
        assertEquals(55.287, copy.getTrackStatistics().getTotalDistance().toM(), 0.01);
    }
}