import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { randomUUID } from 'node:crypto';
import { Repository, enumerateLocalDays } from './repository.js';

describe('Repository calendar_events', () => {
  let tmpDir: string;
  let repo: Repository;

  beforeEach(() => {
    tmpDir = mkdtempSync(join(tmpdir(), 'cal-repo-test-'));
    repo = new Repository(join(tmpDir, 'test.db'));
  });
  afterEach(() => {
    repo.close?.();
    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('creates an event and reads it back', async () => {
    const ev = await repo.createCalendarEvent({
      spaceId: 'space-A',
      ownerId: 'u1',
      date: '2026-06-20',
      time: '14:30',
      title: 'kickoff',
      description: 'first meeting',
      createdBy: 'user',
    });
    expect(ev.id).toBeGreaterThan(0);
    expect(ev.spaceId).toBe('space-A');
    expect(ev.title).toBe('kickoff');
    expect(ev.time).toBe('14:30');
    expect(ev.createdBy).toBe('user');
    expect(ev.sourceTaskId).toBeNull();

    const got = await repo.getCalendarEvent(ev.id);
    expect(got?.title).toBe('kickoff');
  });

  it('defaults: createdBy=user, all-day (time null), description null', async () => {
    const ev = await repo.createCalendarEvent({ spaceId: 'space-A', date: '2026-06-20', title: 'allday' });
    expect(ev.createdBy).toBe('user');
    expect(ev.time).toBeNull();
    expect(ev.description).toBeNull();
    expect(ev.ownerId).toBeNull();
  });

  it('agent event records createdBy=agent and sourceTaskId', async () => {
    const ev = await repo.createCalendarEvent({
      spaceId: 'space-A',
      date: '2026-06-21',
      title: 'agent-added',
      createdBy: 'agent',
      sourceTaskId: 42,
    });
    expect(ev.createdBy).toBe('agent');
    expect(ev.sourceTaskId).toBe(42);
  });

  it('listCalendarEvents isolates by space (A not visible from B)', async () => {
    await repo.createCalendarEvent({ spaceId: 'space-A', date: '2026-06-20', title: 'in-A' });
    await repo.createCalendarEvent({ spaceId: 'space-B', date: '2026-06-20', title: 'in-B' });
    const a = await repo.listCalendarEvents('space-A');
    const b = await repo.listCalendarEvents('space-B');
    expect(a.map(e => e.title)).toEqual(['in-A']);
    expect(b.map(e => e.title)).toEqual(['in-B']);
  });

  it('listCalendarEvents filters by from/to and orders by date,time (all-day first)', async () => {
    await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-22', time: '09:00', title: 'd2-morning' });
    await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-20', time: '15:00', title: 'd0-pm' });
    await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-20', title: 'd0-allday' });
    await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-25', title: 'out-of-range' });
    const list = await repo.listCalendarEvents('S', { from: '2026-06-20', to: '2026-06-23' });
    expect(list.map(e => e.title)).toEqual(['d0-allday', 'd0-pm', 'd2-morning']);
  });

  it('updateCalendarEvent patches fields; deleteCalendarEvent removes', async () => {
    const ev = await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-20', title: 'orig' });
    const upd = await repo.updateCalendarEvent(ev.id, { title: 'renamed', time: '10:00' });
    expect(upd?.title).toBe('renamed');
    expect(upd?.time).toBe('10:00');
    // updating with no fields is a no-op read
    const same = await repo.updateCalendarEvent(ev.id, {});
    expect(same?.title).toBe('renamed');

    await repo.deleteCalendarEvent(ev.id);
    expect(await repo.getCalendarEvent(ev.id)).toBeNull();
  });

  describe('getSpaceCalendarMonth aggregation', () => {
    it('counts events per local date', async () => {
      await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-20', title: 'e1' });
      await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-20', title: 'e2' });
      await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-22', title: 'e3' });
      const res = await repo.getSpaceCalendarMonth('S', {
        monthStart: '2026-06-01', monthEnd: '2026-06-30', tzOffsetMin: 0,
      });
      expect(res.days['2026-06-20']?.eventCount).toBe(2);
      expect(res.days['2026-06-22']?.eventCount).toBe(1);
      expect(res.events).toHaveLength(3);
    });

    it('buckets task created_at by local TZ: 23:30 UTC lands next local day for +540', async () => {
      const space = await repo.createSpace({ kind: 'case', title: 'sp', ownerId: 'u1', visibility: 'private' });
      const task = await repo.createLocalTask({ title: 't1', body: '', spaceId: space.id, ownerId: 'u1' });
      // Force a known UTC created_at at 23:30 on 2026-06-19.
      (repo as any).db
        .prepare(`UPDATE local_tasks SET created_at = ? WHERE id = ?`)
        .run('2026-06-19 23:30:00', task.id);

      // tz=0 → lands on 2026-06-19
      const utc = await repo.getSpaceCalendarMonth(space.id, {
        monthStart: '2026-06-01', monthEnd: '2026-06-30', tzOffsetMin: 0,
      });
      expect(utc.days['2026-06-19']?.taskCount).toBe(1);
      expect(utc.days['2026-06-20']).toBeUndefined();

      // tz=+540 (JST) → 23:30 UTC + 9h = 08:30 next day → 2026-06-20
      const jst = await repo.getSpaceCalendarMonth(space.id, {
        monthStart: '2026-06-01', monthEnd: '2026-06-30', tzOffsetMin: 540,
      });
      expect(jst.days['2026-06-20']?.taskCount).toBe(1);
      expect(jst.days['2026-06-19']).toBeUndefined();
    });
  });

  describe('personal-space calendar includes NULL-space tasks owned by the owner', () => {
    it('month: a personal-workspace task (space_id NULL) is counted in its owner personal space', async () => {
      const personal = await repo.ensurePersonalSpace('owner-1');
      // Personal-workspace tasks are stored with space_id = NULL (by design).
      const t = await repo.createLocalTask({ title: 'personal task', body: '', spaceId: null, ownerId: 'owner-1' });
      (repo as any).db.prepare(`UPDATE local_tasks SET created_at = ? WHERE id = ?`).run('2026-06-20 10:00:00', t.id);
      // A different user's NULL-space task must NOT leak into owner-1's calendar.
      const other = await repo.createLocalTask({ title: 'someone else', body: '', spaceId: null, ownerId: 'owner-2' });
      (repo as any).db.prepare(`UPDATE local_tasks SET created_at = ? WHERE id = ?`).run('2026-06-20 10:00:00', other.id);

      const res = await repo.getSpaceCalendarMonth(personal.id, {
        monthStart: '2026-06-01', monthEnd: '2026-06-30', tzOffsetMin: 0, personalOwnerId: 'owner-1',
      });
      expect(res.days['2026-06-20']?.taskCount).toBe(1); // only owner-1's NULL task
    });

    it('day: a personal-workspace task (space_id NULL) appears in its owner personal space day view', async () => {
      const personal = await repo.ensurePersonalSpace('owner-9');
      const t = await repo.createLocalTask({ title: 'pday', body: '', spaceId: null, ownerId: 'owner-9' });
      (repo as any).db.prepare(`UPDATE local_tasks SET created_at = ? WHERE id = ?`).run('2026-06-20 10:00:00', t.id);
      const day = await repo.getSpaceCalendarDay(personal.id, '2026-06-20', 0, 'owner-9');
      expect(day.tasks.map(x => x.title)).toEqual(['pday']);
    });

    it('case space (personalOwnerId omitted) does NOT pull in NULL-space tasks', async () => {
      const space = await repo.createSpace({ kind: 'case', title: 'C', ownerId: 'u1', visibility: 'private' });
      const t = await repo.createLocalTask({ title: 'null task', body: '', spaceId: null, ownerId: 'u1' });
      (repo as any).db.prepare(`UPDATE local_tasks SET created_at = ? WHERE id = ?`).run('2026-06-20 10:00:00', t.id);
      const res = await repo.getSpaceCalendarMonth(space.id, {
        monthStart: '2026-06-01', monthEnd: '2026-06-30', tzOffsetMin: 0,
      });
      expect(res.days['2026-06-20']).toBeUndefined(); // case space stays strict
    });
  });

  describe('multi-day events (end_date)', () => {
    it('enumerateLocalDays lists inclusive days and handles single/reversed ranges', () => {
      expect(enumerateLocalDays('2026-06-20', '2026-06-23')).toEqual([
        '2026-06-20', '2026-06-21', '2026-06-22', '2026-06-23',
      ]);
      expect(enumerateLocalDays('2026-06-20', '2026-06-20')).toEqual(['2026-06-20']);
      expect(enumerateLocalDays('2026-06-23', '2026-06-20')).toEqual([]);
      // crosses a month boundary
      expect(enumerateLocalDays('2026-06-29', '2026-07-01')).toEqual([
        '2026-06-29', '2026-06-30', '2026-07-01',
      ]);
    });

    it('stores endDate and normalizes end<=start to null (single day)', async () => {
      const span = await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-20', endDate: '2026-06-22', title: 'trip' });
      expect(span.endDate).toBe('2026-06-22');
      const same = await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-20', endDate: '2026-06-20', title: 'sameday' });
      expect(same.endDate).toBeNull();
      const before = await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-20', endDate: '2026-06-18', title: 'bad' });
      expect(before.endDate).toBeNull();
    });

    it('listCalendarEvents returns events that overlap the range (start before / end after)', async () => {
      await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-18', endDate: '2026-06-21', title: 'starts-before' });
      await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-24', endDate: '2026-06-28', title: 'ends-after' });
      await repo.createCalendarEvent({ spaceId: 'S', date: '2026-07-05', title: 'far-future' });
      const list = await repo.listCalendarEvents('S', { from: '2026-06-20', to: '2026-06-25' });
      expect(list.map(e => e.title).sort()).toEqual(['ends-after', 'starts-before']);
    });

    it('getSpaceCalendarMonth counts a span on every covered day, clipped to the month', async () => {
      // 6/29 → 7/2 spanning event; June view should count 6/29 and 6/30 only.
      await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-29', endDate: '2026-07-02', title: 'span' });
      const june = await repo.getSpaceCalendarMonth('S', { monthStart: '2026-06-01', monthEnd: '2026-06-30', tzOffsetMin: 0 });
      expect(june.days['2026-06-29']?.eventCount).toBe(1);
      expect(june.days['2026-06-30']?.eventCount).toBe(1);
      expect(june.days['2026-07-01']).toBeUndefined();
      // July view counts 7/1 and 7/2 (event starts before the month).
      const july = await repo.getSpaceCalendarMonth('S', { monthStart: '2026-07-01', monthEnd: '2026-07-31', tzOffsetMin: 0 });
      expect(july.days['2026-07-01']?.eventCount).toBe(1);
      expect(july.days['2026-07-02']?.eventCount).toBe(1);
      expect(july.days['2026-07-03']).toBeUndefined();
      expect(july.events).toHaveLength(1); // the spanning event is included
    });

    it('getSpaceCalendarDay returns a span on its middle day', async () => {
      await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-20', endDate: '2026-06-24', title: 'conf' });
      const mid = await repo.getSpaceCalendarDay('S', '2026-06-22', 0);
      expect(mid.events.map(e => e.title)).toEqual(['conf']);
      const after = await repo.getSpaceCalendarDay('S', '2026-06-25', 0);
      expect(after.events).toHaveLength(0);
    });

    it('updateCalendarEvent can set and clear endDate', async () => {
      const ev = await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-20', title: 'e' });
      const widened = await repo.updateCalendarEvent(ev.id, { endDate: '2026-06-23' });
      expect(widened?.endDate).toBe('2026-06-23');
      const cleared = await repo.updateCalendarEvent(ev.id, { endDate: null });
      expect(cleared?.endDate).toBeNull();
    });
  });

  describe('end_time (start–end time range)', () => {
    it('stores time + endTime and reads them back', async () => {
      const ev = await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-20', time: '09:00', endTime: '10:30', title: 'mtg' });
      expect(ev.time).toBe('09:00');
      expect(ev.endTime).toBe('10:30');
      const got = await repo.getCalendarEvent(ev.id);
      expect(got?.endTime).toBe('10:30');
    });

    it('drops endTime when there is no start time (all-day)', async () => {
      const ev = await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-20', endTime: '10:30', title: 'allday' });
      expect(ev.time).toBeNull();
      expect(ev.endTime).toBeNull();
    });

    it('updateCalendarEvent can set and clear endTime', async () => {
      const ev = await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-20', time: '09:00', title: 'e' });
      const set = await repo.updateCalendarEvent(ev.id, { endTime: '11:00' });
      expect(set?.endTime).toBe('11:00');
      const cleared = await repo.updateCalendarEvent(ev.id, { endTime: null });
      expect(cleared?.endTime).toBeNull();
    });

    it('clearing the start time also clears endTime (no orphan end time)', async () => {
      const ev = await repo.createCalendarEvent({ spaceId: 'S', date: '2026-06-20', time: '09:00', endTime: '10:00', title: 'e' });
      const cleared = await repo.updateCalendarEvent(ev.id, { time: null });
      expect(cleared?.time).toBeNull();
      expect(cleared?.endTime).toBeNull();
    });
  });

  describe('getSpaceCalendarDay', () => {
    it('returns tasks created that local day + events for that date', async () => {
      const space = await repo.createSpace({ kind: 'case', title: 'sp', ownerId: 'u1', visibility: 'private' });
      const task = await repo.createLocalTask({ title: 'dayTask', body: '', spaceId: space.id, ownerId: 'u1' });
      (repo as any).db
        .prepare(`UPDATE local_tasks SET created_at = ? WHERE id = ?`)
        .run('2026-06-19 23:30:00', task.id);
      await repo.createCalendarEvent({ spaceId: space.id, date: '2026-06-20', title: 'evt' });

      const day = await repo.getSpaceCalendarDay(space.id, '2026-06-20', 540);
      expect(day.tasks.map(t => t.title)).toEqual(['dayTask']);
      expect(day.events.map(e => e.title)).toEqual(['evt']);
      // No event on 2026-06-19 even though the task UTC date is the 19th.
      const prev = await repo.getSpaceCalendarDay(space.id, '2026-06-19', 540);
      expect(prev.tasks).toHaveLength(0);
    });
  });
});
