import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createMigratedTestDb } from '@/tests/helpers/db';
import type { SqliteDatabase } from '@/lib/db/sqlite';
import { UserRepository } from '@/repositories/UserRepository';
import { ProjectRepository } from '@/repositories/ProjectRepository';
import { ProjectMemberRepository } from '@/repositories/ProjectMemberRepository';
import { MeetingRepository } from '@/repositories/MeetingRepository';
import { CalendarRepository } from '@/repositories/CalendarRepository';
import { TodoRepository } from '@/repositories/TodoRepository';
import { NotificationRepository } from '@/repositories/NotificationRepository';
import { ActivityLogRepository } from '@/repositories/ActivityLogRepository';
import { NotificationService } from '@/services/NotificationService';
import { ActivityLogService } from '@/services/ActivityLogService';
import { MeetingService } from '@/services/MeetingService';
import { SseHub } from '@/lib/sse/hub';
import { ForbiddenError } from '@/lib/errors';

function makeService(db: SqliteDatabase) {
  return new MeetingService(
    new MeetingRepository(db),
    new CalendarRepository(db),
    new TodoRepository(db),
    new ProjectMemberRepository(db),
    new NotificationService(new NotificationRepository(db)),
    new ActivityLogService(new ActivityLogRepository(db)),
    new SseHub(),
    db
  );
}

describe('MeetingService', () => {
  let db: SqliteDatabase;
  let service: MeetingService;
  let projectId: number;
  let authorId: number;
  let memberId: number;

  beforeEach(() => {
    db = createMigratedTestDb();
    service = makeService(db);
    const users = new UserRepository(db);
    authorId = users.create({
      name: 'A',
      email: 'a@example.com',
      passwordHash: 'h',
    }).id;
    memberId = users.create({
      name: 'M',
      email: 'm@example.com',
      passwordHash: 'h',
    }).id;
    projectId = new ProjectRepository(db).create({
      name: 'P',
      ownerId: authorId,
    }).id;
    const members = new ProjectMemberRepository(db);
    members.add(projectId, authorId, 'admin');
    members.add(projectId, memberId, 'member');
  });

  afterEach(() => db.close());

  it('creates a meeting, invites members, notifies + logs activity, returns conflicts', () => {
    const { meeting, conflicts } = service.createMeeting(authorId, projectId, {
      title: 'Kickoff',
      startAt: '2026-06-15T10:00:00',
      endAt: '2026-06-15T11:00:00',
      memberIds: [memberId],
    });
    expect(meeting.title).toBe('Kickoff');
    expect(conflicts).toEqual([]);
    expect(new NotificationRepository(db).countUnreadByUser(memberId)).toBe(1);
    expect(
      new ActivityLogRepository(db)
        .findByProject(projectId)
        .items.some((l) => l.action === 'meeting_created')
    ).toBe(true);
    expect(
      new MeetingRepository(db).findMembersByMeeting(meeting.id)
    ).toHaveLength(1);
  });

  describe('checkScheduleConflicts', () => {
    it('detects an overlapping meeting the member attends', () => {
      // 既存ミーティングに member が参加(10:00-11:00)
      const existing = service.createMeeting(authorId, projectId, {
        title: 'existing',
        startAt: '2026-06-15T10:00:00',
        endAt: '2026-06-15T11:00:00',
        memberIds: [memberId],
      }).meeting;
      // 新しいミーティング(10:30-11:30)は重複
      const conflicts = service.checkScheduleConflicts(
        authorId,
        projectId,
        [memberId],
        '2026-06-15T10:30:00',
        '2026-06-15T11:30:00'
      );
      expect(
        conflicts.some((c) => c.type === 'meeting' && c.refId === existing.id)
      ).toBe(true);
    });

    it('does not flag a non-overlapping meeting', () => {
      service.createMeeting(authorId, projectId, {
        title: 'existing',
        startAt: '2026-06-15T10:00:00',
        endAt: '2026-06-15T11:00:00',
        memberIds: [memberId],
      });
      const conflicts = service.checkScheduleConflicts(
        authorId,
        projectId,
        [memberId],
        '2026-06-15T11:00:00',
        '2026-06-15T12:00:00'
      );
      expect(conflicts.filter((c) => c.type === 'meeting')).toHaveLength(0);
    });

    it('excludes the meeting itself via excludeMeetingId', () => {
      const existing = service.createMeeting(authorId, projectId, {
        title: 'existing',
        startAt: '2026-06-15T10:00:00',
        endAt: '2026-06-15T11:00:00',
        memberIds: [memberId],
      }).meeting;
      const conflicts = service.checkScheduleConflicts(
        authorId,
        projectId,
        [memberId],
        '2026-06-15T10:30:00',
        '2026-06-15T11:30:00',
        existing.id
      );
      expect(conflicts.filter((c) => c.type === 'meeting')).toHaveLength(0);
    });

    it('detects an overlapping calendar event created by the member', () => {
      new CalendarRepository(db).create({
        projectId,
        title: 'member event',
        type: 'custom',
        startAt: '2026-06-15T10:00:00',
        endAt: '2026-06-15T11:00:00',
        createdById: memberId,
      });
      const conflicts = service.checkScheduleConflicts(
        authorId,
        projectId,
        [memberId],
        '2026-06-15T10:30:00',
        '2026-06-15T11:30:00'
      );
      expect(conflicts.some((c) => c.type === 'calendar_event')).toBe(true);
    });

    it('detects a high-priority todo due within ±3 days', () => {
      const col = new TodoRepository(db).createColumn({
        projectId,
        name: 'Todo',
        orderIndex: 0,
      }).id;
      new TodoRepository(db).createItem({
        projectId,
        columnId: col,
        title: 'urgent',
        creatorId: authorId,
        assigneeId: memberId,
        priority: 'high',
        dueDate: '2026-06-16',
        orderIndex: 0,
      });
      const conflicts = service.checkScheduleConflicts(
        authorId,
        projectId,
        [memberId],
        '2026-06-15T10:00:00',
        '2026-06-15T11:00:00'
      );
      expect(conflicts.some((c) => c.type === 'important_todo')).toBe(true);
    });

    it('ignores a high-priority todo due outside ±3 days', () => {
      const col = new TodoRepository(db).createColumn({
        projectId,
        name: 'Todo',
        orderIndex: 0,
      }).id;
      new TodoRepository(db).createItem({
        projectId,
        columnId: col,
        title: 'far',
        creatorId: authorId,
        assigneeId: memberId,
        priority: 'high',
        dueDate: '2026-07-01',
        orderIndex: 0,
      });
      const conflicts = service.checkScheduleConflicts(
        authorId,
        projectId,
        [memberId],
        '2026-06-15T10:00:00',
        '2026-06-15T11:00:00'
      );
      expect(conflicts.filter((c) => c.type === 'important_todo')).toHaveLength(
        0
      );
    });
  });

  it('updateMinutes sets minutes', () => {
    const { meeting } = service.createMeeting(authorId, projectId, {
      title: 'M',
      startAt: '2026-06-15T10:00:00',
      endAt: '2026-06-15T11:00:00',
      memberIds: [],
    });
    expect(
      service.updateMinutes(authorId, meeting.id, '# 議事録').minutesMd
    ).toBe('# 議事録');
  });

  it('delete requires creator or admin', () => {
    const { meeting } = service.createMeeting(authorId, projectId, {
      title: 'M',
      startAt: '2026-06-15T10:00:00',
      endAt: '2026-06-15T11:00:00',
      memberIds: [],
    });
    // member(非作成者・非管理者)は削除不可
    expect(() => service.deleteMeeting(memberId, meeting.id)).toThrow(
      ForbiddenError
    );
    service.deleteMeeting(authorId, meeting.id); // 作成者
    expect(new MeetingRepository(db).findById(meeting.id)).toBeNull();
  });
});
