import { describe, expect, it } from 'vitest';
import { nextTick, ref } from 'vue';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import type { Dayjs } from 'dayjs';
import type { Project, Task, TimeEntry } from '@/packages/api/src';
import { useTimesheetGrid } from './useTimesheetGrid';

dayjs.extend(utc);

const WEEK_DAYS = [
    '2026-04-06',
    '2026-04-07',
    '2026-04-08',
    '2026-04-09',
    '2026-04-10',
    '2026-04-11',
    '2026-04-12',
];

function entry(start: string, end: string | null, overrides: Partial<TimeEntry> = {}): TimeEntry {
    const startMs = new Date(start).valueOf();
    const endMs = end ? new Date(end).valueOf() : startMs;

    return {
        id: overrides.id ?? `e-${start}-${end ?? 'running'}`,
        start,
        end,
        duration: end ? Math.floor((endMs - startMs) / 1000) : null,
        description: '',
        member_id: 'm-1',
        project_id: 'p-1',
        task_id: null,
        billable: false,
        tags: [],
        ...overrides,
    } as unknown as TimeEntry;
}

function project(id: string, name: string, isBillable = false): Project {
    return {
        id,
        name,
        is_billable: isBillable,
    } as unknown as Project;
}

function task(id: string, name: string, projectId: string): Task {
    return {
        id,
        name,
        project_id: projectId,
    } as unknown as Task;
}

describe('useTimesheetGrid', () => {
    it('seeds unseen identities and re-sorts seeded rows when project metadata changes', async () => {
        const timeEntries = ref([
            entry('2026-04-10T09:00:00Z', '2026-04-10T10:00:00Z', {
                id: 'seed-b',
                project_id: 'p-b',
                task_id: null,
            }),
            entry('2026-04-10T10:00:00Z', '2026-04-10T11:00:00Z', {
                id: 'seed-a-z',
                project_id: 'p-a',
                task_id: 't-z',
            }),
            entry('2026-04-10T11:00:00Z', '2026-04-10T12:00:00Z', {
                id: 'seed-a-a',
                project_id: 'p-a',
                task_id: 't-a',
            }),
        ]);
        const projects = ref([project('p-a', 'Bravo'), project('p-b', 'Alpha')]);
        const tasks = ref([task('t-z', 'Zulu Task', 'p-a'), task('t-a', 'Alpha Task', 'p-a')]);

        const { rows } = useTimesheetGrid(
            timeEntries,
            ref(WEEK_DAYS),
            projects,
            tasks,
            ref<Dayjs | null>(null)
        );

        expect(rows.value.map((row) => `${row.projectId}:${row.taskId}`)).toEqual([
            'p-b:null',
            'p-a:t-a',
            'p-a:t-z',
        ]);

        projects.value = [project('p-a', 'Aardvark'), project('p-b', 'Zulu')];
        await nextTick();

        expect(rows.value.map((row) => `${row.projectId}:${row.taskId}`)).toEqual([
            'p-a:t-a',
            'p-a:t-z',
            'p-b:null',
        ]);
    });

    it('keeps user-added slots below seeded rows in insertion order', async () => {
        const timeEntries = ref([
            entry('2026-04-10T09:00:00Z', '2026-04-10T10:00:00Z', {
                id: 'seed-b',
                project_id: 'p-seed-b',
            }),
            entry('2026-04-10T10:00:00Z', '2026-04-10T11:00:00Z', {
                id: 'seed-a',
                project_id: 'p-seed-a',
            }),
        ]);
        const projects = ref([
            project('p-seed-a', 'Bravo Seed'),
            project('p-seed-b', 'Alpha Seed'),
            project('p-user-1', 'Alpha User'),
            project('p-user-2', 'Zulu User'),
        ]);

        const { rows, addSlot } = useTimesheetGrid(
            timeEntries,
            ref(WEEK_DAYS),
            projects,
            ref<Task[]>([]),
            ref<Dayjs | null>(null)
        );

        addSlot('p-user-2', null, false, []);
        addSlot('p-user-1', null, false, []);
        await nextTick();

        expect(rows.value.map((row) => row.projectId)).toEqual([
            'p-seed-b',
            'p-seed-a',
            'p-user-2',
            'p-user-1',
        ]);

        projects.value = [
            project('p-seed-a', 'Zulu Seed'),
            project('p-seed-b', 'Alpha Seed'),
            project('p-user-1', 'Aardvark User'),
            project('p-user-2', 'Bravo User'),
        ];
        await nextTick();

        expect(rows.value.map((row) => row.projectId)).toEqual([
            'p-seed-b',
            'p-seed-a',
            'p-user-2',
            'p-user-1',
        ]);
    });

    it('assigns entries to the first duplicate-identity slot and leaves later duplicates empty', async () => {
        const timeEntries = ref<TimeEntry[]>([]);

        const { rows, addSlot } = useTimesheetGrid(
            timeEntries,
            ref(WEEK_DAYS),
            ref([project('p-1', 'Project One')]),
            ref<Task[]>([]),
            ref<Dayjs | null>(null)
        );

        const firstKey = addSlot('p-1', null, false, []);
        const secondKey = addSlot('p-1', null, false, []);
        await nextTick();

        timeEntries.value = [
            entry('2026-04-10T09:00:00Z', '2026-04-10T10:00:00Z', { project_id: 'p-1' }),
        ];
        await nextTick();

        expect(rows.value).toHaveLength(2);
        expect(rows.value[0]?.key).toBe(firstKey);
        expect(rows.value[0]?.totalSeconds).toBe(3600);
        expect(rows.value[1]?.key).toBe(secondKey);
        expect(rows.value[1]?.totalSeconds).toBe(0);
        expect(rows.value[1]?.cells.size).toBe(0);
    });

    it('keeps no-project user slots aligned with refetched no-project entries', async () => {
        const timeEntries = ref<TimeEntry[]>([]);

        const { rows, addSlot } = useTimesheetGrid(
            timeEntries,
            ref(WEEK_DAYS),
            ref<Project[]>([]),
            ref<Task[]>([]),
            ref<Dayjs | null>(null)
        );

        const rowKey = addSlot(null, null, false, []);
        await nextTick();

        expect(rows.value).toHaveLength(1);
        expect(rows.value[0]).toMatchObject({ key: rowKey, projectId: null });

        timeEntries.value = [
            entry('2026-04-10T09:00:00Z', '2026-04-10T10:00:00Z', {
                id: 'no-project',
                project_id: null,
            }),
        ];
        await nextTick();

        expect(rows.value).toHaveLength(1);
        expect(rows.value[0]).toMatchObject({
            key: rowKey,
            projectId: null,
            totalSeconds: 3600,
        });
    });

    it('updates a slot identity in place and clearSlots removes all rows', async () => {
        const { rows, slots, addSlot, updateSlot, clearSlots } = useTimesheetGrid(
            ref<TimeEntry[]>([]),
            ref(WEEK_DAYS),
            ref([project('p-next', 'Next Project')]),
            ref([task('t-1', 'Task One', 'p-next')]),
            ref<Dayjs | null>(null)
        );

        const key = addSlot(null, null, false, []);
        await nextTick();

        updateSlot(key, {
            projectId: 'p-next',
            taskId: 't-1',
            billable: true,
            tags: ['b-tag', 'a-tag'],
        });
        await nextTick();

        expect(rows.value).toHaveLength(1);
        expect(rows.value[0]).toMatchObject({
            key,
            projectId: 'p-next',
            taskId: 't-1',
            billable: true,
            tags: ['a-tag', 'b-tag'],
        });

        clearSlots();
        await nextTick();

        expect(rows.value).toHaveLength(0);
        expect(slots.value).toHaveLength(0);
    });

    it('includes running entries in row and week totals using the live timer clock', () => {
        const currentTime = ref(dayjs.utc('2026-04-10T10:00:00Z'));
        const runningEntry = entry('2026-04-10T09:00:00Z', null, { id: 'running' });

        const { rows, dayTotals, grandTotal } = useTimesheetGrid(
            ref([runningEntry]),
            ref(WEEK_DAYS),
            ref<Project[]>([]),
            ref<Task[]>([]),
            currentTime
        );

        expect(rows.value).toHaveLength(1);
        expect(rows.value[0]?.cells.get(4)?.totalSeconds).toBe(3600);
        expect(rows.value[0]?.totalSeconds).toBe(3600);
        expect(dayTotals.value[4]).toBe(3600);
        expect(grandTotal.value).toBe(3600);

        currentTime.value = dayjs.utc('2026-04-10T11:30:00Z');

        expect(rows.value[0]?.cells.get(4)?.totalSeconds).toBe(9000);
        expect(rows.value[0]?.totalSeconds).toBe(9000);
        expect(dayTotals.value[4]).toBe(9000);
        expect(grandTotal.value).toBe(9000);
    });

    it('always seeds a break row pinned below all other rows when breaks are enabled', async () => {
        const timeEntries = ref([
            entry('2026-04-10T09:00:00Z', '2026-04-10T10:00:00Z', {
                id: 'work-1',
                project_id: 'p-1',
                type: 'work',
            }),
        ]);
        const projects = ref([project('p-1', 'Alpha')]);

        const { rows, addSlot } = useTimesheetGrid(
            timeEntries,
            ref(WEEK_DAYS),
            projects,
            ref<Task[]>([]),
            ref<Dayjs | null>(null),
            ref(true)
        );

        expect(rows.value).toHaveLength(2);
        expect(rows.value[1]?.type).toBe('break');
        expect(rows.value[1]?.totalSeconds).toBe(0);

        // User-added work rows stay above the break row
        addSlot('p-1', null, true, []);
        timeEntries.value = [...timeEntries.value];
        await nextTick();

        expect(rows.value).toHaveLength(3);
        expect(rows.value[2]?.type).toBe('break');
    });

    it('claims break entries for the seeded break row and does not duplicate it', () => {
        const breakEntry = entry('2026-04-10T12:00:00Z', '2026-04-10T12:30:00Z', {
            id: 'break-1',
            project_id: null,
            type: 'break',
        } as Partial<TimeEntry>);

        const { rows } = useTimesheetGrid(
            ref([breakEntry]),
            ref(WEEK_DAYS),
            ref<Project[]>([]),
            ref<Task[]>([]),
            ref<Dayjs | null>(null),
            ref(true)
        );

        expect(rows.value).toHaveLength(1);
        expect(rows.value[0]?.type).toBe('break');
        expect(rows.value[0]?.totalSeconds).toBe(1800);
    });

    it('sums break time into break totals and keeps it out of the worked totals', () => {
        const work = entry('2026-04-10T09:00:00Z', '2026-04-10T10:00:00Z', {
            id: 'work-1',
            project_id: 'p-1',
            type: 'work',
        });
        const brk = entry('2026-04-10T12:00:00Z', '2026-04-10T12:30:00Z', {
            id: 'break-1',
            project_id: null,
            type: 'break',
        } as Partial<TimeEntry>);

        const { dayTotals, grandTotal, breakDayTotals, breakGrandTotal } = useTimesheetGrid(
            ref([work, brk]),
            ref(WEEK_DAYS),
            ref([project('p-1', 'Alpha')]),
            ref<Task[]>([]),
            ref<Dayjs | null>(null),
            ref(true)
        );

        // 2026-04-10 is dayIndex 4. Worked totals see only the 1h work entry.
        expect(dayTotals.value[4]).toBe(3600);
        expect(grandTotal.value).toBe(3600);
        // Break time is tallied separately, per day and for the week.
        expect(breakDayTotals.value[4]).toBe(1800);
        expect(breakGrandTotal.value).toBe(1800);
        // Days without a break contribute nothing.
        expect(breakDayTotals.value[0]).toBe(0);
    });

    it('does not seed a break row when breaks are disabled', () => {
        const { rows } = useTimesheetGrid(
            ref([
                entry('2026-04-10T09:00:00Z', '2026-04-10T10:00:00Z', {
                    id: 'work-1',
                    project_id: 'p-1',
                    type: 'work',
                }),
            ]),
            ref(WEEK_DAYS),
            ref([project('p-1', 'Alpha')]),
            ref<Task[]>([]),
            ref<Dayjs | null>(null),
            ref(false)
        );

        expect(rows.value).toHaveLength(1);
        expect(rows.value[0]?.type).toBe('work');
    });
});
