/*
 * This file is part of focus-timer
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 *
 * Authors: Kamil Prusko <kamilprusko@gmail.com>
 */

namespace Tests
{
    public class CycleTest : Tests.TestSuite
    {
        public CycleTest ()
        {
            this.add_test ("remove", this.test_remove);

            this.add_test ("get_weight", this.test_get_weight);
            this.add_test ("get_completion_time", this.test_get_completion_time);
            this.add_test ("get_completion_time__with_gaps", this.test_get_completion_time__with_gaps);

            this.add_test ("calculate_progress__empty", this.test_calculate_progress__empty);
            this.add_test ("calculate_progress__scheduled", this.test_calculate_progress__scheduled);
            this.add_test ("calculate_progress__in_progress", this.test_calculate_progress__in_progress);
            this.add_test ("calculate_progress__completed", this.test_calculate_progress__completed);
            this.add_test ("calculate_progress__uncompleted", this.test_calculate_progress__uncompleted);
            this.add_test ("calculate_progress__with_gaps", this.test_calculate_progress__with_gaps);

            this.add_test ("is_visible__empty", this.test_is_visible__empty);
            this.add_test ("is_visible__only_uncompleted", this.test_is_visible__only_uncompleted);
            this.add_test ("is_visible__only_zero_weight", this.test_is_visible__only_zero_weight);
            this.add_test ("is_visible__scheduled", this.test_is_visible__scheduled);
            this.add_test ("is_visible__in_progress", this.test_is_visible__in_progress);
            this.add_test ("is_visible__completed", this.test_is_visible__completed);
            this.add_test ("is_visible__mixed_uncompleted_then_scheduled", this.test_is_visible__mixed_uncompleted_then_scheduled);
            this.add_test ("is_visible__mixed_zero_weight_then_weighted", this.test_is_visible__mixed_zero_weight_then_weighted);
        }

        public override void setup ()
        {
            Ft.Timestamp.freeze_to (2000000000 * Ft.Interval.SECOND);
        }

        public override void teardown ()
        {
            Ft.Timestamp.thaw ();

            var settings = Ft.get_settings ();
            settings.revert ();
        }

        public void test_remove ()
        {
            var removed_emitted = 0;
            var weak_notify_emitted = 0;

            var time_block = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block.weak_ref (() => { weak_notify_emitted++; });

            var cycle = new Ft.Cycle ();
            cycle.append (time_block);
            cycle.removed.connect (() => { removed_emitted++; });

            assert_cmpuint (time_block.ref_count, GLib.CompareOperator.EQ, 2);
            cycle.remove (time_block);
            assert_false (cycle.contains (time_block));
            assert_cmpuint (time_block.ref_count, GLib.CompareOperator.EQ, 1);

            time_block = null;
            assert_cmpuint (removed_emitted, GLib.CompareOperator.EQ, 1);
            assert_cmpuint (weak_notify_emitted, GLib.CompareOperator.EQ, 1);
        }

        public void test_get_weight ()
        {
            var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block_1.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.UNCOMPLETED,
                    weight = 1.0,
                }
            );

            var time_block_2 = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block_2.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.SCHEDULED,
                    weight = 1.0,
                }
            );

            var time_block_3 = new Ft.TimeBlock (Ft.State.SHORT_BREAK);
            time_block_3.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.SCHEDULED,
                    weight = 0.0,
                }
            );

            var cycle = new Ft.Cycle ();

            cycle.append (time_block_1);
            assert_cmpfloat_with_epsilon (cycle.get_weight (),
                                          0.0,
                                          0.0001);

            cycle.append (time_block_2);
            assert_cmpfloat_with_epsilon (cycle.get_weight (),
                                          1.0,
                                          0.0001);

            cycle.append (time_block_3);
            assert_cmpfloat_with_epsilon (cycle.get_weight (),
                                          1.0,
                                          0.0001);
        }

        public void test_get_completion_time ()
        {
            var now = Ft.Timestamp.peek ();

            var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block_1.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.UNCOMPLETED,
                    weight = 1.0,
                    completion_time = now + 20 * Ft.Interval.MINUTE,
                }
            );

            var time_block_2 = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block_2.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.SCHEDULED,
                    weight = 1.0,
                    completion_time = now + 25 * Ft.Interval.MINUTE,
                }
            );

            var time_block_3 = new Ft.TimeBlock (Ft.State.SHORT_BREAK);
            time_block_3.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.SCHEDULED,
                    weight = 0.0,
                    completion_time = now + 29 * Ft.Interval.MINUTE,
                }
            );

            var cycle = new Ft.Cycle ();

            cycle.append (time_block_1);
            assert_cmpvariant (
                new GLib.Variant.int64 (cycle.get_completion_time ()),
                new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED)
            );

            cycle.append (time_block_2);
            assert_cmpvariant (
                new GLib.Variant.int64 (cycle.get_completion_time ()),
                new GLib.Variant.int64 (now + 25 * Ft.Interval.MINUTE)
            );

            cycle.append (time_block_3);
            assert_cmpvariant (
                new GLib.Variant.int64 (cycle.get_completion_time ()),
                new GLib.Variant.int64 (now + 25 * Ft.Interval.MINUTE)
            );
        }

        public void test_get_completion_time__with_gaps ()
        {
            var now = Ft.Timestamp.peek ();

            var gap = new Ft.Gap ();
            gap.set_time_range (now + 5 * Ft.Interval.MINUTE, now + 7 * Ft.Interval.MINUTE);  // 2 minutes

            var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block_1.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.IN_PROGRESS,
                    weight = 1.0,
                    intended_duration = 25 * Ft.Interval.MINUTE,
                    completion_time = now + 22 * Ft.Interval.MINUTE,
                }
            );
            time_block_1.add_gap (gap);
            time_block_1.set_time_range (now, now + 27 * Ft.Interval.MINUTE);

            var time_block_2 = new Ft.TimeBlock (Ft.State.SHORT_BREAK);
            time_block_2.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.SCHEDULED,
                    weight = 0.0,
                    completion_time = now + 29 * Ft.Interval.MINUTE,
                }
            );

            var cycle = new Ft.Cycle ();
            cycle.append (time_block_1);
            cycle.append (time_block_2);
            assert_cmpvariant (
                new GLib.Variant.int64 (cycle.get_completion_time ()),
                new GLib.Variant.int64 (now + 22 * Ft.Interval.MINUTE)
            );
        }

        public void test_calculate_progress__empty ()
        {
            var now = Ft.Timestamp.peek ();

            var cycle = new Ft.Cycle ();

            assert_cmpfloat (
                cycle.calculate_progress (now),
                GLib.CompareOperator.EQ,
                0.0
            );
        }

        public void test_calculate_progress__scheduled ()
        {
            var now = Ft.Timestamp.peek ();

            var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block_1.set_time_range (now, now + 25 * Ft.Interval.MINUTE);
            time_block_1.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.SCHEDULED,
                    weight = 1.0,
                    completion_time = now + 20 * Ft.Interval.MINUTE,
                }
            );

            var time_block_2 = new Ft.TimeBlock (Ft.State.SHORT_BREAK);
            time_block_2.set_time_range (now + 25 * Ft.Interval.MINUTE, now + 30 * Ft.Interval.MINUTE);
            time_block_2.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.SCHEDULED,
                    weight = 0.0,
                    completion_time = now + 29 * Ft.Interval.MINUTE,
                }
            );

            var cycle = new Ft.Cycle ();
            cycle.append (time_block_1);
            cycle.append (time_block_2);

            assert_cmpfloat (
                cycle.calculate_progress (time_block_1.start_time),
                GLib.CompareOperator.EQ,
                0.0
            );
            assert_cmpfloat (
                cycle.calculate_progress (time_block_1.end_time),
                GLib.CompareOperator.EQ,
                0.0
            );
            assert_cmpfloat (
                cycle.calculate_progress (time_block_2.end_time),
                GLib.CompareOperator.EQ,
                0.0
            );
        }

        public void test_calculate_progress__in_progress ()
        {
            var now = Ft.Timestamp.peek ();

            var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block_1.set_time_range (now, now + 5 * Ft.Interval.MINUTE);
            time_block_1.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.UNCOMPLETED,
                    weight = 1.0,
                    completion_time = now + 20 * Ft.Interval.MINUTE,
                    intended_duration = 25 * Ft.Interval.MINUTE,
                }
            );

            var time_block_2 = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block_2.set_time_range (now + 5 * Ft.Interval.MINUTE, now + 30 * Ft.Interval.MINUTE);
            time_block_2.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.IN_PROGRESS,
                    weight = 1.0,
                    completion_time = now + 25 * Ft.Interval.MINUTE,
                    intended_duration = 25 * Ft.Interval.MINUTE,
                }
            );

            var time_block_3 = new Ft.TimeBlock (Ft.State.SHORT_BREAK);
            time_block_3.set_time_range (now + 30 * Ft.Interval.MINUTE, now + 35 * Ft.Interval.MINUTE);
            time_block_3.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.SCHEDULED,
                    weight = 0.0,
                    completion_time = now + 29 * Ft.Interval.MINUTE,
                    intended_duration = 5 * Ft.Interval.MINUTE,
                }
            );

            var cycle = new Ft.Cycle ();
            cycle.append (time_block_1);
            cycle.append (time_block_2);
            cycle.append (time_block_3);

            assert_cmpfloat_with_epsilon (cycle.calculate_progress (time_block_1.end_time),
                                          0.0,
                                          0.0001);
            assert_cmpfloat_with_epsilon (cycle.calculate_progress (time_block_2.start_time + 5 * Ft.Interval.MINUTE),
                                          0.25,
                                          0.0001);
            assert_cmpfloat_with_epsilon (cycle.calculate_progress (time_block_2.end_time),
                                          1.0,
                                          0.0001);
            assert_cmpfloat_with_epsilon (cycle.calculate_progress (time_block_3.end_time),
                                          1.0,
                                          0.0001);
        }

        public void test_calculate_progress__completed ()
        {
            var now = Ft.Timestamp.peek ();

            var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block_1.set_time_range (now, now + 25 * Ft.Interval.MINUTE);
            time_block_1.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.COMPLETED,
                    weight = 1.0,
                    completion_time = now + 20 * Ft.Interval.MINUTE,
                    intended_duration = 25 * Ft.Interval.MINUTE,
                }
            );

            var time_block_2 = new Ft.TimeBlock (Ft.State.SHORT_BREAK);
            time_block_2.set_time_range (now + 25 * Ft.Interval.MINUTE, now + 30 * Ft.Interval.MINUTE);
            time_block_2.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.COMPLETED,
                    weight = 0.0,
                    completion_time = now + 29 * Ft.Interval.MINUTE,
                    intended_duration = 5 * Ft.Interval.MINUTE,
                }
            );

            var cycle = new Ft.Cycle ();
            cycle.append (time_block_1);
            cycle.append (time_block_2);

            assert_cmpfloat_with_epsilon (cycle.calculate_progress (time_block_2.end_time),
                                          1.0,
                                          0.0001);
        }

        public void test_calculate_progress__uncompleted ()
        {
            var now = Ft.Timestamp.peek ();

            var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block_1.set_time_range (now, now + 10 * Ft.Interval.MINUTE);
            time_block_1.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.UNCOMPLETED,
                    weight = 0.0,
                    completion_time = now + 20 * Ft.Interval.MINUTE,
                    intended_duration = 25 * Ft.Interval.MINUTE,
                }
            );

            var time_block_2 = new Ft.TimeBlock (Ft.State.SHORT_BREAK);
            time_block_2.set_time_range (now + 10 * Ft.Interval.MINUTE, now + 15 * Ft.Interval.MINUTE);
            time_block_2.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.IN_PROGRESS,
                    weight = 0.0,
                    completion_time = now + 14 * Ft.Interval.MINUTE,
                    intended_duration = 5 * Ft.Interval.MINUTE,
                }
            );

            var cycle = new Ft.Cycle ();
            cycle.append (time_block_1);
            cycle.append (time_block_2);

            assert_cmpfloat (
                cycle.calculate_progress (time_block_2.end_time),
                GLib.CompareOperator.EQ,
                0.0
            );
            assert_cmpfloat (
                cycle.calculate_progress (time_block_2.end_time),
                GLib.CompareOperator.EQ,
                0.0
            );
        }

        public void test_calculate_progress__with_gaps ()
        {
            var now = Ft.Timestamp.peek ();

            var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block_1.set_time_range (now, now + 25 * Ft.Interval.MINUTE);
            time_block_1.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.IN_PROGRESS,
                    weight = 1.0,
                    completion_time = now + 20 * Ft.Interval.MINUTE,
                    intended_duration = 25 * Ft.Interval.MINUTE,
                }
            );
            var gap = new Ft.Gap ();
            gap.set_time_range (now + 5 * Ft.Interval.MINUTE, Ft.Timestamp.UNDEFINED);
            time_block_1.add_gap (gap);

            var time_block_2 = new Ft.TimeBlock (Ft.State.SHORT_BREAK);
            time_block_2.set_time_range (now + 25 * Ft.Interval.MINUTE,
                                         now + 30 * Ft.Interval.MINUTE);
            time_block_2.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.SCHEDULED,
                    weight = 0.0,
                    completion_time = now + 29 * Ft.Interval.MINUTE,
                    intended_duration = 5 * Ft.Interval.MINUTE,
                }
            );

            var cycle = new Ft.Cycle ();
            cycle.append (time_block_1);
            cycle.append (time_block_2);

            assert_cmpfloat_with_epsilon (
                    cycle.calculate_progress (gap.start_time + Ft.Interval.MINUTE),
                    0.25,
                    0.0001);
        }

        public void test_is_visible__empty ()
        {
            var cycle = new Ft.Cycle ();

            assert_false (cycle.is_visible ());
        }

        public void test_is_visible__only_uncompleted ()
        {
            var time_block = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.UNCOMPLETED,
                    weight = 1.0,
                }
            );

            var cycle = new Ft.Cycle ();
            cycle.append (time_block);

            assert_false (cycle.is_visible ());
        }

        public void test_is_visible__only_zero_weight ()
        {
            var time_block = new Ft.TimeBlock (Ft.State.SHORT_BREAK);
            time_block.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.SCHEDULED,
                    weight = 0.0,
                }
            );

            var cycle = new Ft.Cycle ();
            cycle.append (time_block);

            assert_false (cycle.is_visible ());
        }

        public void test_is_visible__scheduled ()
        {
            var time_block = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.SCHEDULED,
                    weight = 1.0
                }
            );

            var cycle = new Ft.Cycle ();
            cycle.append (time_block);

            assert_true (cycle.is_visible ());
        }

        public void test_is_visible__in_progress ()
        {
            var time_block = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.IN_PROGRESS,
                    weight = 1.0
                }
            );

            var cycle = new Ft.Cycle ();
            cycle.append (time_block);

            assert_true (cycle.is_visible ());
        }

        public void test_is_visible__completed ()
        {
            var time_block = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.COMPLETED,
                    weight = 1.0
                }
            );

            var cycle = new Ft.Cycle ();
            cycle.append (time_block);

            assert_true (cycle.is_visible ());
        }

        public void test_is_visible__mixed_uncompleted_then_scheduled ()
        {
            var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block_1.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.UNCOMPLETED,
                    weight = 1.0,
                }
            );

            var time_block_2 = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block_2.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.SCHEDULED,
                    weight = 1.0
                }
            );

            var cycle = new Ft.Cycle ();
            cycle.append (time_block_1);
            cycle.append (time_block_2);

            // Should be visible because second block is scheduled with weight > 0
            assert_true (cycle.is_visible ());
        }

        public void test_is_visible__mixed_zero_weight_then_weighted ()
        {
            var time_block_1 = new Ft.TimeBlock (Ft.State.SHORT_BREAK);
            time_block_1.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.COMPLETED,
                    weight = 0.0,
                }
            );

            var time_block_2 = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block_2.set_meta (
                Ft.TimeBlockMeta () {
                    status = Ft.TimeBlockStatus.SCHEDULED,
                    weight = 1.0
                }
            );

            var cycle = new Ft.Cycle ();
            cycle.append (time_block_1);
            cycle.append (time_block_2);

            // Should be visible because second block has weight > 0 and is scheduled
            assert_true (cycle.is_visible ());
        }
    }
}


public static int main (string[] args)
{
    Tests.init (args);

    return Tests.run (
        new Tests.CycleTest ()
    );
}
