import { describe, expect, it } from 'vitest';

import { ValidationError } from '../errors/index.js';
import {
  computeFacultyLoad,
  detectCapacityConflict,
  detectMeetingConflicts,
  detectStudentScheduleConflicts,
  hasBlockingConflict,
  minutesToTimeString,
  slotsOverlap,
  timeStringToMinutes,
  validateSlot,
  type MeetingSlot,
} from './index.js';

const slot = (overrides: Partial<MeetingSlot> & Pick<MeetingSlot, 'id'>): MeetingSlot => ({
  sectionOfferingId: 'section-1',
  dayOfWeek: 'MONDAY',
  startMinutes: timeStringToMinutes('08:00'),
  endMinutes: timeStringToMinutes('09:30'),
  roomId: 'room-1',
  facultyProfileId: 'faculty-1',
  modality: 'ONSITE',
  ...overrides,
});

describe('time parsing', () => {
  it('converts between minutes and HH:MM', () => {
    expect(timeStringToMinutes('07:30')).toBe(450);
    expect(minutesToTimeString(450)).toBe('07:30');
    expect(minutesToTimeString(0)).toBe('00:00');
    expect(minutesToTimeString(1439)).toBe('23:59');
  });

  it('rejects malformed times', () => {
    for (const value of ['24:00', '7:5', 'noon', '08:60']) {
      expect(() => timeStringToMinutes(value)).toThrow(ValidationError);
    }
  });

  it('rejects a meeting that ends before it starts', () => {
    expect(() => validateSlot(slot({ id: 's', startMinutes: 600, endMinutes: 540 }))).toThrow(
      ValidationError,
    );
  });
});

describe('overlap detection', () => {
  it('treats end times as exclusive, so back-to-back classes do not clash', () => {
    const a = slot({ id: 'a', startMinutes: 480, endMinutes: 540 });
    const b = slot({ id: 'b', startMinutes: 540, endMinutes: 600 });
    expect(slotsOverlap(a, b)).toBe(false);
  });

  it('detects a partial overlap', () => {
    const a = slot({ id: 'a', startMinutes: 480, endMinutes: 570 });
    const b = slot({ id: 'b', startMinutes: 540, endMinutes: 600 });
    expect(slotsOverlap(a, b)).toBe(true);
  });

  it('ignores meetings on different days', () => {
    const a = slot({ id: 'a' });
    const b = slot({ id: 'b', dayOfWeek: 'TUESDAY' });
    expect(slotsOverlap(a, b)).toBe(false);
  });

  it('ignores meetings whose effectivity windows do not overlap', () => {
    const a = slot({
      id: 'a',
      effectiveFrom: new Date('2026-08-01'),
      effectiveTo: new Date('2026-09-01'),
    });
    const b = slot({
      id: 'b',
      effectiveFrom: new Date('2026-10-01'),
      effectiveTo: new Date('2026-11-01'),
    });
    expect(slotsOverlap(a, b)).toBe(false);
  });
});

describe('faculty and room double-booking (SPEC.md §33.12)', () => {
  it('blocks a faculty member scheduled twice at once', () => {
    const conflicts = detectMeetingConflicts([
      slot({ id: 'a', sectionOfferingId: 'section-1', roomId: 'room-1' }),
      slot({ id: 'b', sectionOfferingId: 'section-2', roomId: 'room-2' }),
    ]);

    const faculty = conflicts.filter((conflict) => conflict.kind === 'FACULTY_DOUBLE_BOOKED');
    expect(faculty).toHaveLength(1);
    expect(faculty[0]?.severity).toBe('BLOCK');
    expect(hasBlockingConflict(conflicts)).toBe(true);
  });

  it('blocks a room booked twice at once', () => {
    const conflicts = detectMeetingConflicts([
      slot({ id: 'a', sectionOfferingId: 'section-1', facultyProfileId: 'faculty-1' }),
      slot({ id: 'b', sectionOfferingId: 'section-2', facultyProfileId: 'faculty-2' }),
    ]);
    expect(conflicts.map((conflict) => conflict.kind)).toEqual(['ROOM_DOUBLE_BOOKED']);
  });

  it('reports both when the same faculty and room clash', () => {
    const conflicts = detectMeetingConflicts([
      slot({ id: 'a', sectionOfferingId: 'section-1' }),
      slot({ id: 'b', sectionOfferingId: 'section-2' }),
    ]);
    expect(conflicts.map((conflict) => conflict.kind).sort()).toEqual([
      'FACULTY_DOUBLE_BOOKED',
      'ROOM_DOUBLE_BOOKED',
    ]);
  });

  it('does not contend for a room when the class is online', () => {
    const conflicts = detectMeetingConflicts([
      slot({ id: 'a', sectionOfferingId: 'section-1', modality: 'ONLINE_SYNCHRONOUS', facultyProfileId: 'faculty-1' }),
      slot({ id: 'b', sectionOfferingId: 'section-2', modality: 'ONLINE_SYNCHRONOUS', facultyProfileId: 'faculty-2' }),
    ]);
    expect(conflicts).toEqual([]);
  });

  it('does not book a faculty member for asynchronous online work', () => {
    const conflicts = detectMeetingConflicts([
      slot({ id: 'a', sectionOfferingId: 'section-1', modality: 'ONLINE_ASYNCHRONOUS', roomId: null }),
      slot({ id: 'b', sectionOfferingId: 'section-2', modality: 'ONSITE', roomId: 'room-9' }),
    ]);
    expect(conflicts).toEqual([]);
  });

  it('finds nothing wrong with a clean schedule', () => {
    const conflicts = detectMeetingConflicts([
      slot({ id: 'a', startMinutes: 480, endMinutes: 540 }),
      slot({ id: 'b', startMinutes: 540, endMinutes: 600, sectionOfferingId: 'section-2' }),
      slot({ id: 'c', dayOfWeek: 'WEDNESDAY', sectionOfferingId: 'section-3' }),
    ]);
    expect(conflicts).toEqual([]);
  });
});

describe('student schedule conflicts', () => {
  it('blocks two different subjects that meet at the same time', () => {
    const conflicts = detectStudentScheduleConflicts([
      slot({ id: 'a', sectionOfferingId: 'section-1', facultyProfileId: 'f1', roomId: 'r1' }),
      slot({ id: 'b', sectionOfferingId: 'section-2', facultyProfileId: 'f2', roomId: 'r2' }),
    ]);
    expect(conflicts).toHaveLength(1);
    expect(conflicts[0]?.kind).toBe('STUDENT_SCHEDULE_CONFLICT');
  });

  it('does not flag two meetings of the same section', () => {
    const conflicts = detectStudentScheduleConflicts([
      slot({ id: 'a', sectionOfferingId: 'section-1' }),
      slot({ id: 'b', sectionOfferingId: 'section-1' }),
    ]);
    expect(conflicts).toEqual([]);
  });
});

describe('section capacity', () => {
  it('blocks a full section by default', () => {
    const conflict = detectCapacityConflict(
      { sectionOfferingId: 'section-1', capacity: 40, enrolledCount: 40, sectionName: 'BSN 1-A' },
      false,
    );
    expect(conflict?.severity).toBe('BLOCK');
  });

  it('warns instead when the institution permits overloading', () => {
    const conflict = detectCapacityConflict(
      { sectionOfferingId: 'section-1', capacity: 40, enrolledCount: 41, sectionName: 'BSN 1-A' },
      true,
    );
    expect(conflict?.severity).toBe('WARN');
    expect(hasBlockingConflict(conflict ? [conflict] : [])).toBe(false);
  });

  it('returns nothing when seats remain', () => {
    expect(
      detectCapacityConflict(
        { sectionOfferingId: 'section-1', capacity: 40, enrolledCount: 39, sectionName: 'BSN 1-A' },
        false,
      ),
    ).toBeNull();
  });
});

describe('faculty load computation', () => {
  it('counts hours from actual meetings, not from a units assumption', () => {
    const load = computeFacultyLoad([
      {
        sectionOfferingId: 'section-1',
        lectureUnits: 3,
        laboratoryUnits: 0,
        isLaboratory: false,
        slots: [
          slot({ id: 'a', startMinutes: 480, endMinutes: 570 }),
          slot({ id: 'b', dayOfWeek: 'WEDNESDAY', startMinutes: 480, endMinutes: 570 }),
        ],
      },
      {
        sectionOfferingId: 'section-2',
        lectureUnits: 0,
        laboratoryUnits: 1,
        isLaboratory: true,
        // One unit of laboratory, but three real contact hours.
        slots: [slot({ id: 'c', dayOfWeek: 'FRIDAY', startMinutes: 480, endMinutes: 660 })],
      },
    ]);

    expect(load.sectionCount).toBe(2);
    expect(load.totalUnits).toBe(4);
    expect(load.lectureHoursPerWeek).toBe(3);
    expect(load.laboratoryHoursPerWeek).toBe(3);
    expect(load.totalHoursPerWeek).toBe(6);
  });

  it('returns zeroes for an unassigned faculty member', () => {
    expect(computeFacultyLoad([])).toEqual({
      sectionCount: 0,
      totalUnits: 0,
      lectureHoursPerWeek: 0,
      laboratoryHoursPerWeek: 0,
      totalHoursPerWeek: 0,
    });
  });
});
