import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import express from 'express';
import request from 'supertest';
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, utimesSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { randomUUID } from 'node:crypto';
import { Repository } from '../db/repository.js';
import { createSpaceApi } from './space-api.js';
import { spaceFilesDir } from '../spaces/paths.js';

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

function makeApp(repo: Repository, dataRoot: string, worktreeDir: string, user: TestUser) {
  const app = express();
  app.use((req, _res, next) => {
    (req as any).user = user;
    next();
  });
  app.use('/api/local/spaces', createSpaceApi({ repo, dataRoot, worktreeDir, authActive: true }));
  return app;
}

describe('space-api calendar', () => {
  let repo: Repository;
  let dir: string;
  let worktreeDir: string;
  let dataRoot: string;
  let spaceId: string;

  const owner: TestUser = { id: 'user-1', role: 'user', orgIds: [] };
  const admin: TestUser = { id: 'admin-1', role: 'admin', orgIds: [] };
  const stranger: TestUser = { id: 'user-2', role: 'user', orgIds: [] };

  beforeEach(async () => {
    dir = mkdtempSync(join(tmpdir(), 'space-cal-'));
    dataRoot = join(dir, 'data', 'users');
    worktreeDir = join(dir, 'wt');
    repo = new Repository(join(dir, `${randomUUID()}.db`));
    const space = await repo.createSpace({
      kind: 'case', title: '案件', ownerId: 'user-1', visibility: 'private',
    });
    spaceId = space.id;
  });
  afterEach(() => {
    repo.close();
    rmSync(dir, { recursive: true, force: true });
  });

  function appAs(user: TestUser) {
    return makeApp(repo, dataRoot, worktreeDir, user);
  }

  describe('auth matrix', () => {
    it('owner can create an event (201)', async () => {
      const res = await request(appAs(owner))
        .post(`/api/local/spaces/${spaceId}/calendar/events`)
        .send({ date: '2026-07-01', title: 'mtg', time: '10:00' });
      expect(res.status).toBe(201);
      expect(res.body.title).toBe('mtg');
      expect(res.body.createdBy).toBe('user');
      expect(res.body.ownerId).toBe('user-1');
    });

    it('admin can read the month view (200)', async () => {
      const res = await request(appAs(admin)).get(`/api/local/spaces/${spaceId}/calendar?month=2026-07&tz_offset=540`);
      expect(res.status).toBe(200);
      expect(res.body).toHaveProperty('days');
      expect(res.body).toHaveProperty('events');
    });

    it('non-member gets 404 on month view (private space invisible)', async () => {
      const res = await request(appAs(stranger)).get(`/api/local/spaces/${spaceId}/calendar?month=2026-07`);
      expect(res.status).toBe(404);
    });

    it('non-member gets 404 on event create', async () => {
      const res = await request(appAs(stranger))
        .post(`/api/local/spaces/${spaceId}/calendar/events`)
        .send({ date: '2026-07-01', title: 'x' });
      expect(res.status).toBe(404);
    });
  });

  describe('validation', () => {
    it('bad month → 400', async () => {
      const res = await request(appAs(owner)).get(`/api/local/spaces/${spaceId}/calendar?month=2026-7`);
      expect(res.status).toBe(400);
    });
    it('missing title → 400', async () => {
      const res = await request(appAs(owner))
        .post(`/api/local/spaces/${spaceId}/calendar/events`)
        .send({ date: '2026-07-01' });
      expect(res.status).toBe(400);
    });
    it('bad date → 400', async () => {
      const res = await request(appAs(owner))
        .post(`/api/local/spaces/${spaceId}/calendar/events`)
        .send({ date: '2026/07/01', title: 't' });
      expect(res.status).toBe(400);
    });
    it('bad end_date format → 400', async () => {
      const res = await request(appAs(owner))
        .post(`/api/local/spaces/${spaceId}/calendar/events`)
        .send({ date: '2026-07-01', end_date: '2026/07/03', title: 't' });
      expect(res.status).toBe(400);
    });
    it('end_date before date → 400', async () => {
      const res = await request(appAs(owner))
        .post(`/api/local/spaces/${spaceId}/calendar/events`)
        .send({ date: '2026-07-05', end_date: '2026-07-01', title: 't' });
      expect(res.status).toBe(400);
    });
  });

  describe('end_time (start–end time range) via HTTP', () => {
    it('creates an event with start + end time (201)', async () => {
      const res = await request(appAs(owner))
        .post(`/api/local/spaces/${spaceId}/calendar/events`)
        .send({ date: '2026-07-01', title: 'mtg', time: '09:00', end_time: '10:30' });
      expect(res.status).toBe(201);
      expect(res.body.time).toBe('09:00');
      expect(res.body.endTime).toBe('10:30');
    });

    it('end_time without a start time → 400', async () => {
      const res = await request(appAs(owner))
        .post(`/api/local/spaces/${spaceId}/calendar/events`)
        .send({ date: '2026-07-01', title: 't', end_time: '10:30' });
      expect(res.status).toBe(400);
    });

    it('bad end_time format → 400', async () => {
      const res = await request(appAs(owner))
        .post(`/api/local/spaces/${spaceId}/calendar/events`)
        .send({ date: '2026-07-01', title: 't', time: '09:00', end_time: '25:99' });
      expect(res.status).toBe(400);
    });

    it('single-day end_time before start time → 400', async () => {
      const res = await request(appAs(owner))
        .post(`/api/local/spaces/${spaceId}/calendar/events`)
        .send({ date: '2026-07-01', title: 't', time: '10:00', end_time: '09:00' });
      expect(res.status).toBe(400);
    });

    it('multi-day allows end_time earlier than start time (end is on a later day)', async () => {
      const res = await request(appAs(owner))
        .post(`/api/local/spaces/${spaceId}/calendar/events`)
        .send({ date: '2026-07-01', end_date: '2026-07-03', title: 'trip', time: '18:00', end_time: '09:00' });
      expect(res.status).toBe(201);
      expect(res.body.endTime).toBe('09:00');
    });

    it('PATCH start time only → 400 when it inverts an existing single-day range', async () => {
      // 09:00–10:00 single-day, then move start to 11:00 (end_time omitted) → 11:00 > 10:00 invalid.
      const created = await request(appAs(owner))
        .post(`/api/local/spaces/${spaceId}/calendar/events`)
        .send({ date: '2026-07-01', title: 'x', time: '09:00', end_time: '10:00' });
      const res = await request(appAs(owner))
        .patch(`/api/local/spaces/${spaceId}/calendar/events/${created.body.id}`)
        .send({ time: '11:00' });
      expect(res.status).toBe(400);
    });

    it('PATCH end_date="" → 400 when collapsing a multi-day event leaves end before start', async () => {
      // 18:00 → 09:00 across days is valid; collapsing to single day makes 09:00 < 18:00 invalid.
      const created = await request(appAs(owner))
        .post(`/api/local/spaces/${spaceId}/calendar/events`)
        .send({ date: '2026-07-01', end_date: '2026-07-03', title: 'trip', time: '18:00', end_time: '09:00' });
      const res = await request(appAs(owner))
        .patch(`/api/local/spaces/${spaceId}/calendar/events/${created.body.id}`)
        .send({ end_date: '' });
      expect(res.status).toBe(400);
    });

    it('PATCH sets and clears end_time', async () => {
      const created = await request(appAs(owner))
        .post(`/api/local/spaces/${spaceId}/calendar/events`)
        .send({ date: '2026-07-01', title: 'x', time: '09:00' });
      const set = await request(appAs(owner))
        .patch(`/api/local/spaces/${spaceId}/calendar/events/${created.body.id}`)
        .send({ end_time: '10:00' });
      expect(set.status).toBe(200);
      expect(set.body.endTime).toBe('10:00');
      const cleared = await request(appAs(owner))
        .patch(`/api/local/spaces/${spaceId}/calendar/events/${created.body.id}`)
        .send({ end_time: '' });
      expect(cleared.status).toBe(200);
      expect(cleared.body.endTime).toBeNull();
    });
  });

  describe('multi-day events via HTTP', () => {
    it('creates a span and surfaces it on every covered day in the month view', async () => {
      const created = await request(appAs(owner))
        .post(`/api/local/spaces/${spaceId}/calendar/events`)
        .send({ date: '2026-07-10', end_date: '2026-07-12', title: 'offsite' });
      expect(created.status).toBe(201);
      expect(created.body.endDate).toBe('2026-07-12');

      const month = await request(appAs(owner)).get(`/api/local/spaces/${spaceId}/calendar?month=2026-07&tz_offset=540`);
      expect(month.body.days['2026-07-10']?.eventCount).toBe(1);
      expect(month.body.days['2026-07-11']?.eventCount).toBe(1);
      expect(month.body.days['2026-07-12']?.eventCount).toBe(1);
      expect(month.body.days['2026-07-13']).toBeUndefined();
    });

    it('PATCH end_date="" collapses a span back to a single day', async () => {
      const created = await request(appAs(owner))
        .post(`/api/local/spaces/${spaceId}/calendar/events`)
        .send({ date: '2026-07-10', end_date: '2026-07-12', title: 'x' });
      const patched = await request(appAs(owner))
        .patch(`/api/local/spaces/${spaceId}/calendar/events/${created.body.id}`)
        .send({ end_date: '' });
      expect(patched.status).toBe(200);
      expect(patched.body.endDate).toBeNull();
    });
  });

  describe('event CRUD via HTTP', () => {
    it('create → patch → delete', async () => {
      const created = await request(appAs(owner))
        .post(`/api/local/spaces/${spaceId}/calendar/events`)
        .send({ date: '2026-07-01', title: 'orig' });
      const id = created.body.id;

      const patched = await request(appAs(owner))
        .patch(`/api/local/spaces/${spaceId}/calendar/events/${id}`)
        .send({ title: 'renamed', time: '09:30' });
      expect(patched.status).toBe(200);
      expect(patched.body.title).toBe('renamed');
      expect(patched.body.time).toBe('09:30');

      const del = await request(appAs(owner)).delete(`/api/local/spaces/${spaceId}/calendar/events/${id}`);
      expect(del.status).toBe(204);

      const month = await request(appAs(owner)).get(`/api/local/spaces/${spaceId}/calendar?month=2026-07`);
      expect(month.body.events).toHaveLength(0);
    });
  });

  describe('cross-space event id → 404', () => {
    it('PATCH on an event from another space is 404', async () => {
      const other = await repo.createSpace({ kind: 'case', title: 'B', ownerId: 'user-1', visibility: 'private' });
      const ev = await repo.createCalendarEvent({ spaceId: other.id, date: '2026-07-01', title: 'inB' });
      const res = await request(appAs(owner))
        .patch(`/api/local/spaces/${spaceId}/calendar/events/${ev.id}`)
        .send({ title: 'hijack' });
      expect(res.status).toBe(404);
    });
    it('DELETE on an event from another space is 404', async () => {
      const other = await repo.createSpace({ kind: 'case', title: 'B', ownerId: 'user-1', visibility: 'private' });
      const ev = await repo.createCalendarEvent({ spaceId: other.id, date: '2026-07-01', title: 'inB' });
      const res = await request(appAs(owner)).delete(`/api/local/spaces/${spaceId}/calendar/events/${ev.id}`);
      expect(res.status).toBe(404);
    });
  });

  describe('day endpoint', () => {
    it('returns tasks + events + files for a local day', async () => {
      // task created on local day 2026-07-02 (JST): pick a UTC time inside that day.
      const task = await repo.createLocalTask({ title: 'dayTask', body: '', spaceId, ownerId: 'user-1' });
      (repo as any).db
        .prepare(`UPDATE local_tasks SET created_at = ? WHERE id = ?`)
        .run('2026-07-01 23:30:00', task.id); // +540 → 2026-07-02 08:30

      await repo.createCalendarEvent({ spaceId, date: '2026-07-02', title: 'evt' });

      // place a file with an mtime on the same local day
      const filesDir = spaceFilesDir(worktreeDir, spaceId);
      mkdirSync(filesDir, { recursive: true });
      const fp = join(filesDir, 'report.md');
      writeFileSync(fp, 'hello');
      const mtime = new Date('2026-07-02T03:00:00.000Z'); // +540 → 12:00 local 2026-07-02
      utimesSync(fp, mtime, mtime);

      // a file that should be excluded (under runs/)
      const runsDir = join(filesDir, 'runs', '99');
      mkdirSync(runsDir, { recursive: true });
      const lf = join(runsDir, 'log.txt');
      writeFileSync(lf, 'log');
      utimesSync(lf, mtime, mtime);

      // a file on a different day
      const other = join(filesDir, 'old.md');
      writeFileSync(other, 'x');
      const otherMtime = new Date('2026-06-01T03:00:00.000Z');
      utimesSync(other, otherMtime, otherMtime);

      const res = await request(appAs(owner))
        .get(`/api/local/spaces/${spaceId}/calendar/day?date=2026-07-02&tz_offset=540`);
      expect(res.status).toBe(200);
      expect(res.body.tasks.map((t: any) => t.title)).toEqual(['dayTask']);
      expect(res.body.events.map((e: any) => e.title)).toEqual(['evt']);
      const fileNames = res.body.files.map((f: any) => f.path);
      expect(fileNames).toContain('report.md');
      expect(fileNames).not.toContain('runs/99/log.txt');
      expect(fileNames).not.toContain('old.md');
    });

    it('files is empty array when files dir missing', async () => {
      const res = await request(appAs(owner))
        .get(`/api/local/spaces/${spaceId}/calendar/day?date=2026-07-02&tz_offset=540`);
      expect(res.status).toBe(200);
      expect(res.body.files).toEqual([]);
    });

    it('bad date → 400', async () => {
      const res = await request(appAs(owner)).get(`/api/local/spaces/${spaceId}/calendar/day?date=bad`);
      expect(res.status).toBe(400);
    });
  });
});
