/**
 * Schedule lifecycle coverage — bulkGenerateSchedules, createSchedule's "must have at least
 * one coach assigned" guard (added recently — the exact validation whose ordering broke
 * checkin-cutoff.e2e-spec.ts's fixture helper earlier this session), and
 * TasksService.syncScheduleStatuses' full schedule-level state machine
 * (SCHEDULED -> BOARDING -> EN_ROUTE -> ARRIVED). checkin-cutoff.e2e-spec.ts already covers
 * the per-stop CHECKIN_CLOSED/OPEN half of syncScheduleStatuses — not repeated here.
 */
import { SchedulesService } from "../src/modules/schedules/schedules.service";
import { TasksService } from "../src/modules/tasks/tasks.service";
import { createServiceHarness, ServiceHarness } from "./setup/slim-app";
import { IDS, resetAndSeedCore } from "./fixtures/seed-core";

function asyncStub(): any {
  return new Proxy({}, { get: () => async () => undefined });
}

describe("Schedule lifecycle — bulk generate, coach guard, status transitions", () => {
  let harness: ServiceHarness;
  let schedulesService: SchedulesService;
  let tasksService: TasksService;

  beforeAll(async () => {
    harness = await createServiceHarness();
    schedulesService = await harness.moduleRef.resolve(SchedulesService);
    tasksService = new TasksService(harness.prisma as any, asyncStub(), asyncStub());
  });

  afterAll(async () => {
    await harness?.close();
  });

  beforeEach(async () => {
    await resetAndSeedCore(harness.prisma);
  });

  const future = (mins: number) => new Date(Date.now() + mins * 60_000);

  async function createCoach(trainNumber: string) {
    return harness.prisma.coach.create({
      data: { coachTypeId: IDS.coachType, number: `${trainNumber}-C1`, capacity: 4, sequence: 1, status: "ACTIVE" },
    });
  }

  describe("createSchedule coach-assignment guard", () => {
    it("rejects a schedule with no coachIds and no route coach template", async () => {
      const train = await harness.prisma.train.create({ data: { number: `SCH-NOCOACH-${Date.now()}`, name: "Test" } });

      await expect(
        schedulesService.createSchedule({
          trainId: train.id,
          routeId: IDS.route,
          departureAt: future(180).toISOString(),
          arrivalAt: future(280).toISOString(),
        } as any),
      ).rejects.toThrow(/must have at least one coach assigned/i);

      const orphan = await harness.prisma.trainSchedule.findFirst({ where: { trainId: train.id } });
      expect(orphan).toBeNull(); // no dead schedule left behind
    });

    it("auto-applies the route's coach template when coachIds are omitted", async () => {
      const train = await harness.prisma.train.create({ data: { number: `SCH-TEMPLATE-${Date.now()}`, name: "Test" } });
      const coach = await createCoach(train.number);
      await harness.prisma.routeCoachTemplate.create({
        data: { routeId: IDS.route, coachId: coach.id, positionNumber: 1 },
      });

      const schedule = await schedulesService.createSchedule({
        trainId: train.id,
        routeId: IDS.route,
        departureAt: future(180).toISOString(),
        arrivalAt: future(280).toISOString(),
      } as any);

      const assignments = await harness.prisma.coachAssignment.findMany({ where: { scheduleId: schedule.id } });
      expect(assignments).toHaveLength(1);
      expect(assignments[0].coachId).toBe(coach.id);
    });

    it("explicit coachIds override the route's coach template", async () => {
      const train = await harness.prisma.train.create({ data: { number: `SCH-OVERRIDE-${Date.now()}`, name: "Test" } });
      const templateCoach = await createCoach(`${train.number}-tmpl`);
      const explicitCoach = await createCoach(`${train.number}-explicit`);
      await harness.prisma.routeCoachTemplate.create({
        data: { routeId: IDS.route, coachId: templateCoach.id, positionNumber: 1 },
      });

      const schedule = await schedulesService.createSchedule({
        trainId: train.id,
        routeId: IDS.route,
        departureAt: future(180).toISOString(),
        arrivalAt: future(280).toISOString(),
        coachIds: [explicitCoach.id],
      } as any);

      const assignments = await harness.prisma.coachAssignment.findMany({ where: { scheduleId: schedule.id } });
      expect(assignments).toHaveLength(1);
      expect(assignments[0].coachId).toBe(explicitCoach.id);
    });
  });

  describe("bulkGenerateSchedules", () => {
    it("generates one schedule per repeat interval across the date range", async () => {
      const train = await harness.prisma.train.create({ data: { number: `BULK-${Date.now()}`, name: "Test" } });
      const coach = await createCoach(train.number);
      await harness.prisma.routeCoachTemplate.create({ data: { routeId: IDS.route, coachId: coach.id, positionNumber: 1 } });

      const start = future(2 * 24 * 60); // 2 days out, clear of "today" edge cases
      const result = await schedulesService.bulkGenerateSchedules({
        trainId: train.id,
        routeId: IDS.route,
        startDateTime: start.toISOString(),
        forNextDays: 5,
        repeatEveryDays: 2,
        durationHours: 3,
      } as any);

      // while(currentDate < endDate) stepping by 2 days over a 5-day window: day 0, 2, 4.
      expect(result.schedulesCreated).toBe(3);
      expect(result.errors).toHaveLength(0);
      expect(result.scheduleIds).toHaveLength(3);

      const schedules = await harness.prisma.trainSchedule.findMany({ where: { trainId: train.id }, orderBy: { departureAt: "asc" } });
      expect(schedules).toHaveLength(3);
      const daysBetween = (a: Date, b: Date) => Math.round((b.getTime() - a.getTime()) / (24 * 60 * 60 * 1000));
      expect(daysBetween(schedules[0].departureAt, schedules[1].departureAt)).toBe(2);
      expect(daysBetween(schedules[1].departureAt, schedules[2].departureAt)).toBe(2);
    });

    it("collects per-day errors without aborting the rest of the run", async () => {
      const train = await harness.prisma.train.create({ data: { number: `BULK-ERR-${Date.now()}`, name: "Test" } });
      const coach = await createCoach(train.number);
      await harness.prisma.routeCoachTemplate.create({ data: { routeId: IDS.route, coachId: coach.id, positionNumber: 1 } });

      const start = future(2 * 24 * 60);
      const dto = {
        trainId: train.id,
        routeId: IDS.route,
        startDateTime: start.toISOString(),
        forNextDays: 4,
        repeatEveryDays: 1,
        durationHours: 3,
      } as any;

      const first = await schedulesService.bulkGenerateSchedules(dto);
      expect(first.schedulesCreated).toBe(4);
      expect(first.errors).toHaveLength(0);

      // Re-running the exact same range collides with EVERY day just created (same
      // train+route+date already exists) — proves errors are collected per-day, not thrown,
      // and the count/errors array accurately reflect zero successes this time.
      const second = await schedulesService.bulkGenerateSchedules(dto);
      expect(second.schedulesCreated).toBe(0);
      expect(second.errors).toHaveLength(4);
      expect(second.errors[0]).toMatch(/already exists/i);
    });
  });

  describe("syncScheduleStatuses — schedule-level state machine", () => {
    async function scheduleWithCoach(trainNumber: string, departureAt: Date, arrivalAt: Date) {
      const train = await harness.prisma.train.create({ data: { number: trainNumber, name: "Test" } });
      const coach = await createCoach(trainNumber);
      return schedulesService.createSchedule({
        trainId: train.id,
        routeId: IDS.route,
        departureAt: departureAt.toISOString(),
        arrivalAt: arrivalAt.toISOString(),
        coachIds: [coach.id],
      } as any);
    }

    it("SCHEDULED -> BOARDING once within the 30-min cutoff window", async () => {
      const schedule = await scheduleWithCoach(`SYNC-BOARD-${Date.now()}`, future(20), future(80));
      expect((await harness.prisma.trainSchedule.findUnique({ where: { id: schedule.id } }))?.status).toBe("SCHEDULED");

      await tasksService.syncScheduleStatuses();

      expect((await harness.prisma.trainSchedule.findUnique({ where: { id: schedule.id } }))?.status).toBe("BOARDING");
    });

    it("BOARDING -> EN_ROUTE once departure has passed", async () => {
      const schedule = await scheduleWithCoach(`SYNC-ENROUTE-${Date.now()}`, future(20), future(80));
      await tasksService.syncScheduleStatuses();
      await harness.prisma.trainSchedule.update({ where: { id: schedule.id }, data: { departureAt: new Date(Date.now() - 60_000) } });

      await tasksService.syncScheduleStatuses();

      expect((await harness.prisma.trainSchedule.findUnique({ where: { id: schedule.id } }))?.status).toBe("EN_ROUTE");
    });

    it("EN_ROUTE -> ARRIVED once arrival has passed", async () => {
      const schedule = await scheduleWithCoach(`SYNC-ARRIVED-${Date.now()}`, future(20), future(80));
      await tasksService.syncScheduleStatuses();
      await harness.prisma.trainSchedule.update({
        where: { id: schedule.id },
        data: { departureAt: new Date(Date.now() - 120_000), arrivalAt: new Date(Date.now() - 60_000) },
      });

      await tasksService.syncScheduleStatuses();
      await tasksService.syncScheduleStatuses(); // BOARDING->EN_ROUTE and EN_ROUTE->ARRIVED are separate updateMany calls in one pass — one call suffices, second call is a no-op idempotency check

      expect((await harness.prisma.trainSchedule.findUnique({ where: { id: schedule.id } }))?.status).toBe("ARRIVED");
    });
  });
});
