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 } from './repository.js';
import { spaceColor } from '../spaces/space-color.js';

type TestUser = { id: string; role: 'user' | 'admin'; orgIds: string[] };

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

  const u1: TestUser = { id: 'u1', role: 'user', orgIds: [] };
  const u2: TestUser = { id: 'u2', role: 'user', orgIds: [] };
  const admin: TestUser = { id: 'admin', role: 'admin', orgIds: [] };

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

  const month = { monthStart: '2026-06-01', monthEnd: '2026-06-30', tzOffsetMin: 0 };

  it('aggregates events per day per space across 2+ visible spaces', async () => {
    const a = await repo.createSpace({ kind: 'case', title: 'Alpha', ownerId: 'u1', visibility: 'private' });
    const b = await repo.createSpace({ kind: 'case', title: 'Beta', ownerId: 'u1', visibility: 'private' });
    await repo.createCalendarEvent({ spaceId: a.id, date: '2026-06-10', title: 'a1' });
    await repo.createCalendarEvent({ spaceId: a.id, date: '2026-06-10', title: 'a2' });
    await repo.createCalendarEvent({ spaceId: b.id, date: '2026-06-10', title: 'b1' });
    await repo.createCalendarEvent({ spaceId: b.id, date: '2026-06-12', title: 'b2' });

    const res = await repo.getCrossSpaceCalendarMonth(u1 as any, month);

    expect(res.days['2026-06-10']?.[a.id]?.eventCount).toBe(2);
    expect(res.days['2026-06-10']?.[b.id]?.eventCount).toBe(1);
    expect(res.days['2026-06-12']?.[b.id]?.eventCount).toBe(1);
    expect(res.days['2026-06-12']?.[a.id]).toBeUndefined();
    // events carry their spaceId so the UI can bucket per-space.
    expect(res.events).toHaveLength(4);
    expect(res.events.every(e => e.spaceId === a.id || e.spaceId === b.id)).toBe(true);
    expect(res.spaces.map(s => s.id).sort()).toEqual([a.id, b.id].sort());
  });

  it('counts tasks per day per space', async () => {
    const a = await repo.createSpace({ kind: 'case', title: 'A', ownerId: 'u1', visibility: 'private' });
    const t1 = await repo.createLocalTask({ title: 't1', body: '', spaceId: a.id, ownerId: 'u1' });
    const t2 = await repo.createLocalTask({ title: 't2', body: '', spaceId: a.id, ownerId: 'u1' });
    (repo as any).db.prepare(`UPDATE local_tasks SET created_at = ? WHERE id = ?`).run('2026-06-15 08:00:00', t1.id);
    (repo as any).db.prepare(`UPDATE local_tasks SET created_at = ? WHERE id = ?`).run('2026-06-15 09:00:00', t2.id);

    const res = await repo.getCrossSpaceCalendarMonth(u1 as any, month);
    expect(res.days['2026-06-15']?.[a.id]?.taskCount).toBe(2);
  });

  describe('visibility — never leak non-visible spaces', () => {
    it('a private space owned by someone else is absent (events + counts + legend)', async () => {
      const mine = await repo.createSpace({ kind: 'case', title: 'Mine', ownerId: 'u1', visibility: 'private' });
      const theirs = await repo.createSpace({ kind: 'case', title: 'Theirs', ownerId: 'u2', visibility: 'private' });
      await repo.createCalendarEvent({ spaceId: mine.id, date: '2026-06-10', title: 'mine-evt' });
      await repo.createCalendarEvent({ spaceId: theirs.id, date: '2026-06-10', title: 'secret' });
      const task = await repo.createLocalTask({ title: 'secret-task', body: '', spaceId: theirs.id, ownerId: 'u2' });
      (repo as any).db.prepare(`UPDATE local_tasks SET created_at = ? WHERE id = ?`).run('2026-06-10 08:00:00', task.id);

      const res = await repo.getCrossSpaceCalendarMonth(u1 as any, month);

      // legend excludes the other user's space
      expect(res.spaces.map(s => s.id)).toEqual([mine.id]);
      // no counts bucket for the invisible space
      expect(res.days['2026-06-10']?.[theirs.id]).toBeUndefined();
      expect(res.days['2026-06-10']?.[mine.id]?.eventCount).toBe(1);
      // no events from the invisible space
      expect(res.events.map(e => e.title)).toEqual(['mine-evt']);
      expect(res.events.some(e => e.spaceId === theirs.id)).toBe(false);
    });

    it('admin sees all spaces', async () => {
      const a = await repo.createSpace({ kind: 'case', title: 'A', ownerId: 'u1', visibility: 'private' });
      const b = await repo.createSpace({ kind: 'case', title: 'B', ownerId: 'u2', visibility: 'private' });
      await repo.createCalendarEvent({ spaceId: a.id, date: '2026-06-10', title: 'a' });
      await repo.createCalendarEvent({ spaceId: b.id, date: '2026-06-10', title: 'b' });

      const res = await repo.getCrossSpaceCalendarMonth(admin as any, month);
      expect(res.spaces.map(s => s.id).sort()).toEqual([a.id, b.id].sort());
      expect(res.events).toHaveLength(2);
    });

    it('non-member sees only public / own spaces', async () => {
      const pub = await repo.createSpace({ kind: 'case', title: 'Public', ownerId: 'u1', visibility: 'public' });
      const priv = await repo.createSpace({ kind: 'case', title: 'Private', ownerId: 'u1', visibility: 'private' });
      await repo.createCalendarEvent({ spaceId: pub.id, date: '2026-06-10', title: 'pub-evt' });
      await repo.createCalendarEvent({ spaceId: priv.id, date: '2026-06-10', title: 'priv-evt' });

      const res = await repo.getCrossSpaceCalendarMonth(u2 as any, month);
      expect(res.spaces.map(s => s.id)).toEqual([pub.id]);
      expect(res.events.map(e => e.title)).toEqual(['pub-evt']);
    });
  });

  it('buckets task created_at by local TZ (boundary): 23:30 UTC → next local day for +540', async () => {
    const a = await repo.createSpace({ kind: 'case', title: 'A', ownerId: 'u1', visibility: 'private' });
    const task = await repo.createLocalTask({ title: 't', body: '', spaceId: a.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);

    const utc = await repo.getCrossSpaceCalendarMonth(u1 as any, { monthStart: '2026-06-01', monthEnd: '2026-06-30', tzOffsetMin: 0 });
    expect(utc.days['2026-06-19']?.[a.id]?.taskCount).toBe(1);
    expect(utc.days['2026-06-20']?.[a.id]).toBeUndefined();

    const jst = await repo.getCrossSpaceCalendarMonth(u1 as any, { monthStart: '2026-06-01', monthEnd: '2026-06-30', tzOffsetMin: 540 });
    expect(jst.days['2026-06-20']?.[a.id]?.taskCount).toBe(1);
    expect(jst.days['2026-06-19']?.[a.id]).toBeUndefined();
  });

  describe('space color', () => {
    it('uses brand_color when set', async () => {
      const a = await repo.createSpace({ kind: 'case', title: 'A', ownerId: 'u1', visibility: 'private', brandColor: '#123456' });
      const res = await repo.getCrossSpaceCalendarMonth(u1 as any, month);
      expect(res.spaces.find(s => s.id === a.id)?.color).toBe('#123456');
    });

    it('derives a stable color from id when brand_color is null (matches shared spaceColor)', async () => {
      const a = await repo.createSpace({ kind: 'case', title: 'A', ownerId: 'u1', visibility: 'private' });
      const res1 = await repo.getCrossSpaceCalendarMonth(u1 as any, month);
      const res2 = await repo.getCrossSpaceCalendarMonth(u1 as any, month);
      const c1 = res1.spaces.find(s => s.id === a.id)?.color;
      expect(c1).toBe(spaceColor(a.id));
      // stable across calls
      expect(res2.spaces.find(s => s.id === a.id)?.color).toBe(c1);
    });
  });

  it('returns empty shape when the viewer can see no spaces', async () => {
    await repo.createSpace({ kind: 'case', title: 'Theirs', ownerId: 'u1', visibility: 'private' });
    const res = await repo.getCrossSpaceCalendarMonth(u2 as any, month);
    expect(res.spaces).toEqual([]);
    expect(res.events).toEqual([]);
    expect(res.days).toEqual({});
  });

  it('counts PERSONAL-space tasks (space_id NULL, owner-linked) under the personal space column', async () => {
    // Personal conversations are created with space_id NULL but owner_id set; the
    // per-space personal calendar shows them via an owner fallback. The cross
    // calendar must do the same so they reflect in the overall calendar.
    const ps = await repo.ensurePersonalSpace('u1');
    const t = await repo.createLocalTask({ title: 'mine', body: '', ownerId: 'u1' }); // no spaceId → space_id NULL
    expect((await repo.getLocalTask(t.id))?.spaceId ?? null).toBeNull();
    (repo as any).db.prepare(`UPDATE local_tasks SET created_at = ? WHERE id = ?`).run('2026-06-20 08:00:00', t.id);

    const res = await repo.getCrossSpaceCalendarMonth(u1 as any, month);

    // The personal space is a visible column, and its NULL-space task is counted there.
    expect(res.spaces.some(s => s.id === ps.id)).toBe(true);
    expect(res.days['2026-06-20']?.[ps.id]?.taskCount).toBe(1);
  });
});
