/*
 * 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 SessionTemplateTest : Tests.TestSuite
    {
        public SessionTemplateTest ()
        {
            this.add_test ("calculate_break_percentage", this.test_calculate_break_percentage);
        }

        public void test_calculate_break_percentage ()
        {
            var session_template_1 = Ft.SessionTemplate () {
                pomodoro_duration = 25 * Ft.Interval.MINUTE,
                short_break_duration = 0 * Ft.Interval.MINUTE,
                long_break_duration = 0 * Ft.Interval.MINUTE,
                cycles = 4
            };
            assert_cmpfloat_with_epsilon (session_template_1.calculate_break_percentage (), 0.0, 0.0001);

            var session_template_2 = Ft.SessionTemplate () {
                pomodoro_duration = 0 * Ft.Interval.MINUTE,
                short_break_duration = 5 * Ft.Interval.MINUTE,
                long_break_duration = 15 * Ft.Interval.MINUTE,
                cycles = 4
            };
            assert_cmpfloat_with_epsilon (session_template_2.calculate_break_percentage (), 100.0, 0.0001);

            var session_template_3 = Ft.SessionTemplate () {
                pomodoro_duration = 3 * Ft.Interval.MINUTE,
                short_break_duration = 1 * Ft.Interval.MINUTE,
                long_break_duration = 1 * Ft.Interval.MINUTE,
                cycles = 1
            };
            // 1 / (3 + 1)
            assert_cmpfloat_with_epsilon (session_template_3.calculate_break_percentage (), 25.0, 0.0001);

            var session_template_4 = Ft.SessionTemplate () {
                pomodoro_duration = 25 * Ft.Interval.MINUTE,
                short_break_duration = 5 * Ft.Interval.MINUTE,
                long_break_duration = 10 * Ft.Interval.MINUTE,
                cycles = 4
            };
            // (5 + 5 + 5 + 10) / (25 + 5 + 25 + 5 + 25 + 5 + 25 + 10)
            assert_cmpfloat_with_epsilon (session_template_4.calculate_break_percentage (), 20.0, 0.0001);
        }
    }


    public class SessionTest : Tests.TestSuite
    {
        private Ft.SessionTemplate session_template = Ft.SessionTemplate () {
            pomodoro_duration = 25 * Ft.Interval.MINUTE,
            short_break_duration = 5 * Ft.Interval.MINUTE,
            long_break_duration = 15 * Ft.Interval.MINUTE,
            cycles = 4
        };

        public SessionTest ()
        {
            this.add_test ("new", this.test_new);
            this.add_test ("new_from_template", this.test_new_from_template);

            this.add_test ("duration", this.test_duration);
            this.add_test ("start_time", this.test_start_time);
            this.add_test ("end_time", this.test_end_time);

            this.add_test ("get_first_time_block", this.test_get_first_time_block);
            this.add_test ("get_last_time_block", this.test_get_last_time_block);
            this.add_test ("get_next_time_block", this.test_get_next_time_block);
            this.add_test ("get_previous_time_block", this.test_get_previous_time_block);
            this.add_test ("append", this.test_append);
            this.add_test ("prepend", this.test_prepend);
            this.add_test ("insert_before", this.test_insert_before);
            this.add_test ("insert_after", this.test_insert_after);
            this.add_test ("contains", this.test_contains);
            this.add_test ("move_by", this.test_move_by);
            this.add_test ("move_to", this.test_move_to);
            this.add_test ("remove", this.test_remove);
            this.add_test ("remove_before", this.test_remove_before);
            this.add_test ("remove_after", this.test_remove_after);

            this.add_test ("set_time_block_status__completed", this.test_set_time_block_status__completed);
            this.add_test ("set_time_block_status__uncompleted", this.test_set_time_block_status__uncompleted);
            this.add_test ("is_expired", this.test_is_expired);
            this.add_test ("calculate_elapsed", this.test_calculate_elapsed);
            this.add_test ("calculate_remaining", this.test_calculate_remaining);
            // this.add_test ("calculate_progress", this.test_calculate_progress);
            // this.add_test ("calculate_pomodoro_break_ratio", this.test_calculate_pomodoro_break_ratio);
            this.add_test ("calculate_break_ratio", this.test_calculate_break_ratio);
            this.add_test ("get_cycles", this.test_get_cycles);
            this.add_test ("get_cycles__remove_time_block", this.test_get_cycles__remove_time_block);

            // TODO: Tests for signals
            // this.add_test ("freeze_changed", this.test_freeze_changed);
            // this.add_test ("changed_signal", this.test_changed_signal);
            // this.add_test ("added_signal", this.test_time_block_added_signal);
            // this.add_test ("removed_signal", this.test_time_block_removed_signal);
        }

        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 ();
        }

        /**
         * Check constructor `Session()`.
         *
         * Expect session not to have any time-blocks.
         */
        public void test_new ()
        {
            var session = new Ft.Session ();

            var first_time_block = session.get_first_time_block ();
            assert_null (first_time_block);

            var last_time_block = session.get_last_time_block ();
            assert_null (last_time_block);

            assert_cmpvariant (
                new GLib.Variant.int64 (session.start_time),
                new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED)
            );
            assert_cmpvariant (
                new GLib.Variant.int64 (session.end_time),
                new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED)
            );
            // assert_cmpuint (session.get_cycles ().length (), GLib.CompareOperator.EQ, 0);
        }

        /**
         * Check constructor `Session.from_template()`.
         *
         * Expect session to have time-blocks defined according to settings.
         */
        public void test_new_from_template ()
        {
            var now = Ft.Timestamp.advance (0);
            var template = this.session_template;
            var session = new Ft.Session.from_template (template);
            Ft.TimeBlock[] time_blocks = {};

            session.@foreach ((time_block) => {
                time_blocks += time_block;
            });

            assert_cmpuint (time_blocks.length, GLib.CompareOperator.EQ, template.cycles * 2);

            // Expect:
            //   - pomodoro / break states to be interleaved
            //   - last break to be a long one
            //   - start times to align
            //   - `cycle` to be indexed form 1
            var expected_start_time = now;

            for (uint index=0; index < time_blocks.length; index++)
            {
                var time_block = time_blocks[index];

                if ((index & 1) == 0) {
                    assert_true (time_block.state == Ft.State.POMODORO);
                    assert_cmpvariant (
                        new GLib.Variant.int64 (time_block.duration),
                        new GLib.Variant.int64 (template.pomodoro_duration)
                    );
                }
                else if (index < time_blocks.length - 1) {
                    assert_true (time_block.state == Ft.State.SHORT_BREAK);
                    assert_cmpvariant (
                        new GLib.Variant.int64 (time_block.duration),
                        new GLib.Variant.int64 (template.short_break_duration)
                    );
                }
                else {
                    assert_true (time_block.state == Ft.State.LONG_BREAK);
                    assert_cmpvariant (
                        new GLib.Variant.int64 (time_block.duration),
                        new GLib.Variant.int64 (template.long_break_duration)
                    );
                }

                assert_cmpvariant (
                    new GLib.Variant.int64 (time_block.start_time),
                    new GLib.Variant.int64 (expected_start_time)
                );
                assert_true (time_block.session == session);
                // assert_cmpuint (meta.cycle, GLib.CompareOperator.EQ, 1 + (index >> 1));

                expected_start_time += time_block.duration;
            }

            assert_cmpvariant (
                new GLib.Variant.int64 (session.start_time),
                new GLib.Variant.int64 (now)
            );
            assert_cmpvariant (
                new GLib.Variant.int64 (session.end_time),
                new GLib.Variant.int64 (
                    session.start_time + (
                        template.pomodoro_duration * template.cycles +
                        template.short_break_duration * (template.cycles - 1) +
                        template.long_break_duration
                    )
                )
            );
            // assert_cmpuint (session.get_cycles ().length (), GLib.CompareOperator.EQ, template.cycles);
        }

        public void test_duration ()
        {
            var notify_duration_emitted = 0;
            var now = Ft.Timestamp.advance (0);

            var session_0 = new Ft.Session ();
            assert_cmpvariant (
                new GLib.Variant.int64 (session_0.duration),
                new GLib.Variant.int64 (0)
            );

            var session_1 = new Ft.Session ();
            session_1.notify["duration"].connect (() => {
                notify_duration_emitted++;
            });

            var time_block_1 = new Ft.TimeBlock ();
            time_block_1.set_time_range (now, now + 2 * Ft.Interval.MINUTE);
            session_1.append (time_block_1);
            assert_cmpvariant (
                new GLib.Variant.int64 (session_1.duration),
                new GLib.Variant.int64 (2 * Ft.Interval.MINUTE)
            );
            assert_cmpuint (notify_duration_emitted, GLib.CompareOperator.EQ, 1);

            var time_block_2 = new Ft.TimeBlock ();
            time_block_2.set_time_range (now, now + 3 * Ft.Interval.MINUTE);
            session_1.append (time_block_2);
            assert_cmpvariant (
                new GLib.Variant.int64 (session_1.duration),
                new GLib.Variant.int64 (5 * Ft.Interval.MINUTE)
            );
            assert_cmpuint (notify_duration_emitted, GLib.CompareOperator.EQ, 2);

            // Adding a gap between time-blocks should increase duration.
            time_block_2.move_by (Ft.Interval.MINUTE);
            assert_cmpvariant (
                new GLib.Variant.int64 (session_1.duration),
                new GLib.Variant.int64 (6 * Ft.Interval.MINUTE)
            );
            assert_cmpuint (notify_duration_emitted, GLib.CompareOperator.EQ, 3);

            // Moving a session shouldn't emit notify signal.
            session_1.move_by (Ft.Interval.MINUTE);
            assert_cmpvariant (
                new GLib.Variant.int64 (session_1.duration),
                new GLib.Variant.int64 (6 * Ft.Interval.MINUTE)
            );
            assert_cmpuint (notify_duration_emitted, GLib.CompareOperator.EQ, 3);

            // Uncompleted time-blocks should be included in duration.
            session_1.set_time_block_status (time_block_2, Ft.TimeBlockStatus.UNCOMPLETED);
            assert_cmpvariant (
                new GLib.Variant.int64 (session_1.duration),
                new GLib.Variant.int64 (6 * Ft.Interval.MINUTE)
            );
            assert_cmpuint (notify_duration_emitted, GLib.CompareOperator.EQ, 3);
        }

        public void test_start_time ()
        {
            var notify_start_time_emitted = 0;
            var now = Ft.Timestamp.advance (0);

            var session_0 = new Ft.Session ();
            assert_cmpvariant (
                new GLib.Variant.int64 (session_0.start_time),
                new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED)
            );

            var session_1 = new Ft.Session ();
            session_1.notify["start-time"].connect (() => {
                notify_start_time_emitted++;
            });

            var time_block_2 = new Ft.TimeBlock ();
            time_block_2.set_time_range (now + 4 * Ft.Interval.MINUTE,
                                         now + 5 * Ft.Interval.MINUTE);
            session_1.prepend (time_block_2);
            assert_cmpvariant (
                new GLib.Variant.int64 (session_1.start_time),
                new GLib.Variant.int64 (time_block_2.start_time)
            );
            assert_cmpuint (notify_start_time_emitted, GLib.CompareOperator.EQ, 1);

            var time_block_1 = new Ft.TimeBlock ();
            time_block_1.set_time_range (now + 3 * Ft.Interval.MINUTE,
                                         now + 4 * Ft.Interval.MINUTE);
            session_1.prepend (time_block_1);
            assert_cmpvariant (
                new GLib.Variant.int64 (session_1.start_time),
                new GLib.Variant.int64 (time_block_1.start_time)
            );
            assert_cmpuint (notify_start_time_emitted, GLib.CompareOperator.EQ, 2);

            // Moving first time-block should affect start_time.
            var expected_start_time = time_block_1.start_time + Ft.Interval.MINUTE;
            time_block_1.move_by (Ft.Interval.MINUTE);
            assert_cmpvariant (
                new GLib.Variant.int64 (session_1.start_time),
                new GLib.Variant.int64 (expected_start_time)
            );
            assert_cmpuint (notify_start_time_emitted, GLib.CompareOperator.EQ, 3);

            // Moving a session should emit notify signal.
            expected_start_time = session_1.start_time + Ft.Interval.MINUTE;
            session_1.move_to (expected_start_time);
            assert_cmpvariant (
                new GLib.Variant.int64 (session_1.start_time),
                new GLib.Variant.int64 (expected_start_time)
            );
            assert_cmpuint (notify_start_time_emitted, GLib.CompareOperator.EQ, 4);

            // Moving second time-block should not affect start_time.
            expected_start_time = session_1.start_time;
            time_block_2.move_to (time_block_2.start_time + Ft.Interval.MINUTE);
            assert_cmpvariant (
                new GLib.Variant.int64 (session_1.start_time),
                new GLib.Variant.int64 (expected_start_time)
            );
            assert_cmpuint (notify_start_time_emitted, GLib.CompareOperator.EQ, 4);

            // Uncompleted time-blocks should be included in start_time.
            expected_start_time = session_1.start_time;
            session_1.set_time_block_status (time_block_1, Ft.TimeBlockStatus.UNCOMPLETED);
            assert_cmpvariant (
                new GLib.Variant.int64 (session_1.start_time),
                new GLib.Variant.int64 (expected_start_time)
            );
            assert_cmpuint (notify_start_time_emitted, GLib.CompareOperator.EQ, 4);
        }

        public void test_end_time ()
        {
            var notify_end_time_emitted = 0;
            var now = Ft.Timestamp.advance (0);

            var session_0 = new Ft.Session ();
            assert_cmpvariant (
                new GLib.Variant.int64 (session_0.end_time),
                new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED)
            );

            var session_1 = new Ft.Session ();
            session_1.notify["end-time"].connect (() => {
                notify_end_time_emitted++;
            });

            var time_block_1 = new Ft.TimeBlock ();
            time_block_1.set_time_range (now + 3 * Ft.Interval.MINUTE,
                                         now + 4 * Ft.Interval.MINUTE);
            session_1.append (time_block_1);
            assert_cmpvariant (
                new GLib.Variant.int64 (session_1.end_time),
                new GLib.Variant.int64 (time_block_1.end_time)
            );
            assert_cmpuint (notify_end_time_emitted, GLib.CompareOperator.EQ, 1);

            var time_block_2 = new Ft.TimeBlock ();
            time_block_2.set_time_range (now + 4 * Ft.Interval.MINUTE,
                                         now + 5 * Ft.Interval.MINUTE);
            session_1.append (time_block_2);
            assert_cmpvariant (
                new GLib.Variant.int64 (session_1.end_time),
                new GLib.Variant.int64 (time_block_2.end_time)
            );
            assert_cmpuint (notify_end_time_emitted, GLib.CompareOperator.EQ, 2);

            // Moving second time-block should affect end_time.
            var expected_end_time = time_block_2.end_time + Ft.Interval.MINUTE;
            time_block_2.move_by (Ft.Interval.MINUTE);
            assert_cmpvariant (
                new GLib.Variant.int64 (session_1.end_time),
                new GLib.Variant.int64 (expected_end_time)
            );
            assert_cmpuint (notify_end_time_emitted, GLib.CompareOperator.EQ, 3);

            // Moving a session should emit notify signal.
            expected_end_time = session_1.end_time + Ft.Interval.MINUTE;
            session_1.move_by (Ft.Interval.MINUTE);
            assert_cmpvariant (
                new GLib.Variant.int64 (session_1.end_time),
                new GLib.Variant.int64 (expected_end_time)
            );
            assert_cmpuint (notify_end_time_emitted, GLib.CompareOperator.EQ, 4);

            // Moving first time-block should not affect end_time.
            expected_end_time = session_1.end_time;
            time_block_1.move_by (-Ft.Interval.MINUTE);
            assert_cmpvariant (
                new GLib.Variant.int64 (session_1.end_time),
                new GLib.Variant.int64 (expected_end_time)
            );
            assert_cmpuint (notify_end_time_emitted, GLib.CompareOperator.EQ, 4);

            // Uncompleted time-blocks should be included in end_time.
            expected_end_time = session_1.end_time;
            session_1.set_time_block_status (time_block_1, Ft.TimeBlockStatus.UNCOMPLETED);
            assert_cmpvariant (
                new GLib.Variant.int64 (session_1.end_time),
                new GLib.Variant.int64 (expected_end_time)
            );
            assert_cmpuint (notify_end_time_emitted, GLib.CompareOperator.EQ, 4);
        }

        public void test_get_first_time_block ()
        {
            var time_blocks = new Ft.TimeBlock[0];
            var session     = new Ft.Session.from_template (this.session_template);
            session.@foreach ((time_block) => {
                time_blocks += time_block;
            });

            assert_true (session.get_first_time_block () == time_blocks[0]);

            var empty_session = new Ft.Session ();
            assert_null (empty_session.get_first_time_block ());
        }

        public void test_get_last_time_block ()
        {
            var time_blocks = new Ft.TimeBlock[0];
            var session     = new Ft.Session.from_template (this.session_template);
            session.@foreach ((time_block) => {
                time_blocks += time_block;
            });

            assert_true (session.get_last_time_block () == time_blocks[7]);

            var empty_session = new Ft.Session ();
            assert_null (empty_session.get_last_time_block ());
        }

        public void test_get_next_time_block ()
        {
            var time_blocks = new Ft.TimeBlock[0];
            var session     = new Ft.Session.from_template (this.session_template);
            session.@foreach ((time_block) => {
                time_blocks += time_block;
            });

            assert_true (
                session.get_next_time_block (time_blocks[0]) == time_blocks[1]
            );
            assert_true (
                session.get_next_time_block (time_blocks[1]) == time_blocks[2]
            );
            assert_null (
                session.get_next_time_block (time_blocks[7])
            );
            assert_null (
                session.get_next_time_block (new Ft.TimeBlock (Ft.State.POMODORO))
            );
        }

        public void test_get_previous_time_block ()
        {
            var time_blocks = new Ft.TimeBlock[0];
            var session     = new Ft.Session.from_template (this.session_template);
            session.@foreach ((time_block) => {
                time_blocks += time_block;
            });

            assert_true (
                session.get_previous_time_block (time_blocks[2]) == time_blocks[1]
            );
            assert_true (
                session.get_previous_time_block (time_blocks[1]) == time_blocks[0]
            );
            assert_null (
                session.get_previous_time_block (time_blocks[0])
            );
            assert_null (
                session.get_previous_time_block (new Ft.TimeBlock (Ft.State.POMODORO))
            );
        }

        public void test_append ()
        {
            var session = new Ft.Session ();
            var now     = Ft.Timestamp.from_now ();

            var added_emitted = 0;
            session.added.connect (() => {
                added_emitted++;
            });

            var changed_emitted = 0;
            session.changed.connect (() => {
                changed_emitted++;
            });

            var time_block_1 = new Ft.TimeBlock ();
            time_block_1.set_time_range (now, now + Ft.Interval.MINUTE);
            session.append (time_block_1);
            assert_cmpuint (added_emitted, GLib.CompareOperator.EQ, 1);
            assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 1);
            assert_true (session.get_last_time_block () == time_block_1);
            assert_cmpvariant (
                new GLib.Variant.int64 (time_block_1.start_time),
                new GLib.Variant.int64 (now)
            );

            var time_block_2 = new Ft.TimeBlock (Ft.State.POMODORO);
            time_block_2.set_time_range (now, now + Ft.Interval.MINUTE);
            session.append (time_block_2);
            assert_cmpuint (added_emitted, GLib.CompareOperator.EQ, 2);
            assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 2);
            assert_true (session.get_last_time_block () == time_block_2);
            assert_cmpvariant (
                new GLib.Variant.int64 (time_block_2.start_time),
                new GLib.Variant.int64 (time_block_1.end_time)
            );
        }

        public void test_prepend ()
        {
            var session = new Ft.Session ();

            var added_emitted = 0;
            session.added.connect (() => {
                added_emitted++;
            });

            var changed_emitted = 0;
            session.changed.connect (() => {
                changed_emitted++;
            });

            var time_block_1 = new Ft.TimeBlock ();
            session.prepend (time_block_1);
            assert_cmpuint (added_emitted, GLib.CompareOperator.EQ, 1);
            assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 1);
            assert_true (session.get_first_time_block () == time_block_1);

            var time_block_2 = new Ft.TimeBlock ();
            session.prepend (time_block_2);
            assert_cmpuint (added_emitted, GLib.CompareOperator.EQ, 2);
            assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 2);
            assert_true (session.get_first_time_block () == time_block_2);
        }

        public void test_insert_before ()
        {
            var session = new Ft.Session.from_template (this.session_template);

            var added_emitted = 0;
            session.added.connect (() => {
                added_emitted++;
            });

            var changed_emitted = 0;
            session.changed.connect (() => {
                changed_emitted++;
            });

            var reference_time_block = session.get_last_time_block ();

            var time_block_1 = new Ft.TimeBlock ();
            session.insert_before (time_block_1, reference_time_block);
            assert_cmpuint (added_emitted, GLib.CompareOperator.EQ, 1);
            assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 1);
            assert_true (session.get_previous_time_block (reference_time_block) == time_block_1);

            var time_block_2 = new Ft.TimeBlock ();
            session.insert_before (time_block_2, session.get_first_time_block ());
            assert_cmpuint (added_emitted, GLib.CompareOperator.EQ, 2);
            assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 2);
            assert_true (session.get_first_time_block () == time_block_2);
        }

        public void test_insert_after ()
        {
            var session = new Ft.Session.from_template (this.session_template);

            var added_emitted = 0;
            session.added.connect (() => {
                added_emitted++;
            });

            var changed_emitted = 0;
            session.changed.connect (() => {
                changed_emitted++;
            });

            var reference_time_block = session.get_last_time_block ();

            var time_block_1 = new Ft.TimeBlock ();
            session.insert_after (time_block_1, reference_time_block);
            assert_cmpuint (added_emitted, GLib.CompareOperator.EQ, 1);
            assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 1);
            assert_true (session.get_next_time_block (reference_time_block) == time_block_1);

            var time_block_2 = new Ft.TimeBlock ();
            session.insert_after (time_block_2, session.get_last_time_block ());
            assert_cmpuint (added_emitted, GLib.CompareOperator.EQ, 2);
            assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 2);
            assert_true (session.get_last_time_block () == time_block_2);
        }

        public void test_contains ()
        {
            var session = new Ft.Session.from_template (this.session_template);
            var time_block = new Ft.TimeBlock ();

            assert_false (session.contains (time_block));

            session.append (time_block);
            assert_true (session.contains (time_block));
        }

        public void test_move_by ()
        {
            var session = new Ft.Session.from_template (this.session_template);
            var first_time_block = session.get_first_time_block ();

            var changed_emitted = 0;
            session.changed.connect (() => {
                changed_emitted++;
            });

            var expected_start_time = session.start_time + Ft.Interval.MINUTE;
            session.move_by (Ft.Interval.MINUTE);
            assert_cmpvariant (
                new GLib.Variant.int64 (session.start_time),
                new GLib.Variant.int64 (expected_start_time)
            );
            assert_cmpvariant (
                new GLib.Variant.int64 (first_time_block.start_time),
                new GLib.Variant.int64 (expected_start_time)
            );
            assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 1);
        }

        public void test_move_to ()
        {
            var session = new Ft.Session.from_template (this.session_template);
            var first_time_block = session.get_first_time_block ();

            var changed_emitted = 0;
            session.changed.connect (() => {
                changed_emitted++;
            });

            var expected_start_time = session.start_time + Ft.Interval.MINUTE;
            session.move_to (expected_start_time);
            assert_cmpvariant (
                new GLib.Variant.int64 (session.start_time),
                new GLib.Variant.int64 (expected_start_time)
            );
            assert_cmpvariant (
                new GLib.Variant.int64 (first_time_block.start_time),
                new GLib.Variant.int64 (expected_start_time)
            );
            assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 1);
        }

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

            var session = new Ft.Session.from_template (this.session_template);
            session.removed.connect (() => { removed_emitted++; });

            var time_block = session.get_last_time_block ();
            time_block.weak_ref (() => { weak_notify_emitted++; });

            assert_cmpuint (time_block.ref_count, GLib.CompareOperator.EQ, 2);
            session.remove (time_block);
            assert_false (session.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_remove_before ()
        {
            var removed_emitted = 0;
            var weak_notify_emitted = 0;

            var session = new Ft.Session.from_template (this.session_template);
            session.removed.connect (() => { removed_emitted++; });

            var time_block_1 = session.get_first_time_block ();
            time_block_1.weak_ref (() => { weak_notify_emitted++; });

            var time_block_2 = session.get_last_time_block ();

            assert_cmpuint (time_block_1.ref_count, GLib.CompareOperator.EQ, 2);
            session.remove_before (time_block_2);
            assert_false (session.contains (time_block_1));
            assert_true (session.contains (time_block_2));
            assert_cmpuint (time_block_1.ref_count, GLib.CompareOperator.EQ, 1);

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

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

            var session = new Ft.Session.from_template (this.session_template);
            session.removed.connect (() => { removed_emitted++; });

            var time_block_1 = session.get_first_time_block ();

            var time_block_2 = session.get_last_time_block ();
            time_block_2.weak_ref (() => { weak_notify_emitted++; });

            assert_cmpuint (time_block_2.ref_count, GLib.CompareOperator.EQ, 2);
            session.remove_after (time_block_1);
            assert_true (session.contains (time_block_1));
            assert_false (session.contains (time_block_2));
            assert_cmpuint (time_block_2.ref_count, GLib.CompareOperator.EQ, 1);

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

        public void test_set_time_block_status__completed ()
        {
            var session = new Ft.Session.from_template (this.session_template);

            var time_block_1 = session.get_nth_time_block (0);
            var time_block_2 = session.get_nth_time_block (1);
            var time_block_3 = session.get_nth_time_block (2);

            session.set_time_block_status (time_block_1, Ft.TimeBlockStatus.COMPLETED);
            assert_cmpuint (time_block_1.get_status (),
                            GLib.CompareOperator.EQ,
                            Ft.TimeBlockStatus.COMPLETED);
            assert_cmpuint (time_block_2.get_status (),
                            GLib.CompareOperator.EQ,
                            Ft.TimeBlockStatus.SCHEDULED);

            session.set_time_block_status (time_block_1, Ft.TimeBlockStatus.UNCOMPLETED);
            session.set_time_block_status (time_block_3, Ft.TimeBlockStatus.COMPLETED);
            assert_cmpuint (time_block_3.get_status (),
                            GLib.CompareOperator.EQ,
                            Ft.TimeBlockStatus.COMPLETED);
            assert_cmpuint (time_block_2.get_status (),
                            GLib.CompareOperator.EQ,
                            Ft.TimeBlockStatus.UNCOMPLETED);  // change from scheduled
            assert_cmpuint (time_block_1.get_status (),
                            GLib.CompareOperator.EQ,
                            Ft.TimeBlockStatus.UNCOMPLETED);  // no change
        }

        public void test_set_time_block_status__uncompleted ()
        {
            var session = new Ft.Session.from_template (this.session_template);

            var time_block_1 = session.get_nth_time_block (0);
            var time_block_2 = session.get_nth_time_block (1);
            var time_block_3 = session.get_nth_time_block (2);

            session.set_time_block_status (time_block_1, Ft.TimeBlockStatus.UNCOMPLETED);
            assert_cmpuint (time_block_1.get_status (),
                            GLib.CompareOperator.EQ,
                            Ft.TimeBlockStatus.UNCOMPLETED);
            assert_cmpuint (time_block_2.get_status (),
                            GLib.CompareOperator.EQ,
                            Ft.TimeBlockStatus.SCHEDULED);

            session.set_time_block_status (time_block_1, Ft.TimeBlockStatus.COMPLETED);
            session.set_time_block_status (time_block_3, Ft.TimeBlockStatus.UNCOMPLETED);
            assert_cmpuint (time_block_3.get_status (),
                            GLib.CompareOperator.EQ,
                            Ft.TimeBlockStatus.UNCOMPLETED);
            assert_cmpuint (time_block_2.get_status (),
                            GLib.CompareOperator.EQ,
                            Ft.TimeBlockStatus.UNCOMPLETED);  // change from scheduled
            assert_cmpuint (time_block_1.get_status (),
                            GLib.CompareOperator.EQ,
                            Ft.TimeBlockStatus.COMPLETED);  // no change
        }

        public void test_is_expired ()
        {
            var now = Ft.Timestamp.advance (0);
            var session = new Ft.Session.from_template (this.session_template);
            assert_false (session.is_expired (now));

            session.expiry_time = now + Ft.Interval.MINUTE;
            assert_false (session.is_expired (now));
            assert_true (session.is_expired (now + Ft.Interval.MINUTE));
        }

        public void test_calculate_elapsed ()
        {
            // TODO
        }

        public void test_calculate_remaining ()
        {
            // TODO
        }

        // public void test_calculate_progress ()
        // {
        // }

        // public void test_calculate_pomodoro_break_ratio ()
        // {
        //     var session = new Ft.Session ();
        //
        //     assert_cmpfloat_with_epsilon (
        //         session.calculate_pomodoro_break_ratio (),
        //         double.INFINITY,
        //         0.0001
        //     );
        // }

        public void test_calculate_break_ratio ()
        {
            var session = new Ft.Session.from_template (
                Ft.SessionTemplate () {
                    pomodoro_duration = 25 * Ft.Interval.MINUTE,
                    short_break_duration = 5 * Ft.Interval.MINUTE,
                    long_break_duration = 10 * Ft.Interval.MINUTE,
                    cycles = 2
                }
            );
            var time_block_1 = session.get_nth_time_block (0);
            var time_block_2 = session.get_nth_time_block (1);
            var time_block_3 = session.get_nth_time_block (2);
            var time_block_4 = session.get_nth_time_block (3);

            assert_cmpfloat_with_epsilon (session.calculate_break_ratio (time_block_1.end_time),
                                          0.0,
                                          0.0001);
            assert_cmpfloat_with_epsilon (session.calculate_break_ratio (time_block_2.end_time),
                                          5.0 / 30.0,
                                          0.0001);
            assert_cmpfloat_with_epsilon (session.calculate_break_ratio (time_block_3.end_time),
                                          5.0 / 55.0,
                                          0.0001);
            assert_cmpfloat_with_epsilon (session.calculate_break_ratio (time_block_4.end_time),
                                          15.0 / 65.0,
                                          0.0001);
        }

        public void test_get_cycles ()
        {
            var session_0 = new Ft.Session ();
            assert_cmpuint (session_0.get_cycles ().length (), GLib.CompareOperator.EQ, 0);

            var session_1 = new Ft.Session.from_template (
                Ft.SessionTemplate () {
                    pomodoro_duration = 25 * Ft.Interval.MINUTE,
                    short_break_duration = 5 * Ft.Interval.MINUTE,
                    long_break_duration = 15 * Ft.Interval.MINUTE,
                    cycles = 1
                }
            );
            assert_cmpuint (session_1.get_cycles ().length (), GLib.CompareOperator.EQ, 1);

            var session_2 = new Ft.Session.from_template (
                Ft.SessionTemplate () {
                    pomodoro_duration = 25 * Ft.Interval.MINUTE,
                    short_break_duration = 5 * Ft.Interval.MINUTE,
                    long_break_duration = 15 * Ft.Interval.MINUTE,
                    cycles = 2
                }
            );
            assert_cmpuint (session_2.get_cycles ().length (), GLib.CompareOperator.EQ, 2);

            // When session starts with a break, expect cycles to be 0.
            var session_3 = new Ft.Session ();
            session_3.append (new Ft.TimeBlock (Ft.State.SHORT_BREAK));
            assert_cmpuint (session_3.get_cycles ().length (), GLib.CompareOperator.EQ, 1);
            session_3.append (new Ft.TimeBlock (Ft.State.POMODORO));
            assert_cmpuint (session_3.get_cycles ().length (), GLib.CompareOperator.EQ, 2);

            // When pomodoros are after one another, expect cycle to increment; though it's not a true cycle.
            // In normal case pomodoro would be extended and counted properly.
            var session_4 = new Ft.Session ();
            session_4.append (new Ft.TimeBlock (Ft.State.POMODORO));
            assert_cmpuint (session_4.get_cycles ().length (), GLib.CompareOperator.EQ, 1);
            session_4.append (new Ft.TimeBlock (Ft.State.POMODORO));
            assert_cmpuint (session_4.get_cycles ().length (), GLib.CompareOperator.EQ, 2);

            // Treat undefined states same as breaks.
            var session_5 = new Ft.Session ();
            session_5.append (new Ft.TimeBlock (Ft.State.STOPPED));
            assert_cmpuint (session_5.get_cycles ().length (), GLib.CompareOperator.EQ, 1);
            session_5.append (new Ft.TimeBlock (Ft.State.POMODORO));
            assert_cmpuint (session_5.get_cycles ().length (), GLib.CompareOperator.EQ, 2);

            // Cycles with uncompleted time-blocks still should be included.
            var session_6 = new Ft.Session ();
            var uncompleted_time_block = new Ft.TimeBlock (Ft.State.POMODORO);
            session_6.append (uncompleted_time_block);
            session_6.set_time_block_status (uncompleted_time_block, Ft.TimeBlockStatus.UNCOMPLETED);
            assert_cmpuint (session_6.get_cycles ().length (), GLib.CompareOperator.EQ, 1);
            session_6.append (new Ft.TimeBlock (Ft.State.POMODORO));
            assert_cmpuint (session_6.get_cycles ().length (), GLib.CompareOperator.EQ, 2);
        }

        /**
         * Cycles do not own time-blocks per say. Ensure there is no segfault after removing
         * time-block from a session.
         */
        public void test_get_cycles__remove_time_block ()
        {
            var session = new Ft.Session.from_template (this.session_template);
            var cycle = session.get_cycles ().first ().data;

            weak Ft.TimeBlock time_block = session.get_first_time_block ();

            assert_true (cycle.contains (time_block));
            session.remove (time_block);
            assert_true (cycle.contains (time_block));
        }
    }
}


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

    return Tests.run (
        new Tests.SessionTemplateTest (),
        new Tests.SessionTest ()
    );
}
