import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
import Fastify from "fastify";
import cookie from "@fastify/cookie";
import rateLimit from "@fastify/rate-limit";
import jwt from "jsonwebtoken";
import prisma from "../config/database";
import { config } from "../config/env";
import { securityHeaders } from "../middleware/security";
import attendanceRoutes from "../routes/admin/attendance";

// ---------------------------------------------------------------------------
// Regression tests for the attendance create/edit wire format.
//
// The admin create/edit forms send COMBINED local datetimes
// ("YYYY-MM-DDTHH:MM:00", built by combineDatetime from a date + time input)
// for arrival_time / departure_time / break_start / break_end, and the
// service parses them with `new Date(...)`. Commit 519edce validated these
// fields with `timeString` (bare HH:MM), which rejected every create/edit
// containing a time, and CreateAttendanceSchema lacked break_start/break_end
// entirely, so breaks were silently stripped on create. These tests pin the
// combined-datetime contract at the HTTP route level.
// ---------------------------------------------------------------------------

// All fixtures live on far-future dates so they can never collide with real
// rows in the throwaway test DB; cleanup deletes exactly this range.
const RANGE_START = new Date("2098-01-01T00:00:00");
const RANGE_END = new Date("2099-01-01T00:00:00");

let app: Awaited<ReturnType<typeof buildApp>>;
let adminUserId: number;
let adminToken: string;

async function buildApp() {
  const a = Fastify({ logger: false });
  await a.register(cookie);
  await a.register(rateLimit, { max: 1000, timeWindow: "1 minute" });
  a.addHook("onRequest", securityHeaders);
  await a.register(attendanceRoutes, { prefix: "/api/admin/attendance" });
  return a;
}

/**
 * Generate a JWT access token. `requireAuth` re-loads auth data from the DB
 * via `loadAuthData(payload.sub)`, so only the `sub` (user id) matters here.
 */
function generateToken(user: {
  id: number;
  username: string;
  roleName: string | null;
}): string {
  return jwt.sign(
    { sub: user.id, username: user.username, role: user.roleName },
    config.jwt.secret,
    { expiresIn: "15m" },
  );
}

async function authPost(path: string, token: string, body: unknown) {
  return app.inject({
    method: "POST",
    url: path,
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    },
    payload: body as object,
  });
}

async function authPut(path: string, token: string, body: unknown) {
  return app.inject({
    method: "PUT",
    url: path,
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    },
    payload: body as object,
  });
}

async function cleanupFixtureRows() {
  await prisma.attendance.deleteMany({
    where: {
      user_id: adminUserId,
      shift_date: { gte: RANGE_START, lt: RANGE_END },
    },
  });
}

beforeAll(async () => {
  app = await buildApp();

  const admin = await prisma.users.findFirst({
    where: { roles: { name: "admin" } },
    include: { roles: true },
  });
  if (!admin) throw new Error("Test setup: admin user not found");
  adminUserId = admin.id;
  adminToken = generateToken({
    id: admin.id,
    username: admin.username,
    roleName: admin.roles?.name ?? null,
  });

  await cleanupFixtureRows();
});

beforeEach(async () => {
  await cleanupFixtureRows();
});

afterAll(async () => {
  await cleanupFixtureRows();
  if (app) await app.close();
});

describe("POST /api/admin/attendance (combined datetimes)", () => {
  it("creates a work record with arrival+departure datetimes and persists them", async () => {
    const arrival = "2098-03-10T08:00:00";
    const departure = "2098-03-10T16:30:00";

    const res = await authPost("/api/admin/attendance", adminToken, {
      user_id: adminUserId,
      shift_date: "2098-03-10",
      leave_type: "work",
      arrival_time: arrival,
      departure_time: departure,
      notes: "attendance_wire_test work",
    });

    expect(res.statusCode).toBe(201);
    const body = res.json();
    expect(body.success).toBe(true);

    const rec = await prisma.attendance.findUnique({
      where: { id: body.data.id },
    });
    expect(rec).toBeTruthy();
    expect(rec!.leave_type).toBe("work");
    expect(rec!.arrival_time?.getTime()).toBe(new Date(arrival).getTime());
    expect(rec!.departure_time?.getTime()).toBe(new Date(departure).getTime());
  });

  it("persists break_start/break_end on create (previously silently stripped)", async () => {
    const res = await authPost("/api/admin/attendance", adminToken, {
      user_id: adminUserId,
      shift_date: "2098-03-11",
      leave_type: "work",
      arrival_time: "2098-03-11T08:00:00",
      departure_time: "2098-03-11T16:30:00",
      break_start: "2098-03-11T12:00:00",
      break_end: "2098-03-11T12:30:00",
      notes: "attendance_wire_test break",
    });

    expect(res.statusCode).toBe(201);
    const body = res.json();
    expect(body.success).toBe(true);

    const rec = await prisma.attendance.findUnique({
      where: { id: body.data.id },
    });
    expect(rec!.break_start?.getTime()).toBe(
      new Date("2098-03-11T12:00:00").getTime(),
    );
    expect(rec!.break_end?.getTime()).toBe(
      new Date("2098-03-11T12:30:00").getTime(),
    );
  });

  it("creates a leave record with NO time fields", async () => {
    const res = await authPost("/api/admin/attendance", adminToken, {
      user_id: adminUserId,
      shift_date: "2098-03-12",
      leave_type: "vacation",
      leave_hours: 8,
      notes: "attendance_wire_test leave",
    });

    expect(res.statusCode).toBe(201);
    const body = res.json();
    expect(body.success).toBe(true);

    const rec = await prisma.attendance.findUnique({
      where: { id: body.data.id },
    });
    expect(rec!.leave_type).toBe("vacation");
    expect(Number(rec!.leave_hours)).toBe(8);
    expect(rec!.arrival_time).toBeNull();
    expect(rec!.departure_time).toBeNull();
  });

  it('tolerates "" for unset datetime fields (old form payloads) → null', async () => {
    // The AttendanceCreate page historically always sent empty strings for
    // the unfilled time fields — even for leave records — which 400'd EVERY
    // submit after 519edce. The schema must treat "" as "not set".
    const res = await authPost("/api/admin/attendance", adminToken, {
      user_id: adminUserId,
      shift_date: "2098-03-13",
      leave_type: "sick",
      leave_hours: 8,
      arrival_time: "",
      departure_time: "",
      break_start: "",
      break_end: "",
      notes: "attendance_wire_test empty strings",
    });

    expect(res.statusCode).toBe(201);
    const body = res.json();
    expect(body.success).toBe(true);

    const rec = await prisma.attendance.findUnique({
      where: { id: body.data.id },
    });
    expect(rec!.leave_type).toBe("sick");
    expect(rec!.arrival_time).toBeNull();
    expect(rec!.departure_time).toBeNull();
    expect(rec!.break_start).toBeNull();
    expect(rec!.break_end).toBeNull();
  });
});

describe("GET /api/admin/attendance (complete month, no truncation)", () => {
  it("returns every record of a month with more than 100 rows", async () => {
    // The admin month view computes the KPI cards and summary totals from
    // this single fetch, so it must cover the COMPLETE month. With the old
    // 100-row pagination cap, months with >100 records were silently
    // truncated (newest-first) and the screen totals dropped the earliest
    // days while the print stayed complete.
    const rows = [];
    for (let day = 1; day <= 30; day++) {
      for (let s = 0; s < 4; s++) {
        rows.push({
          user_id: adminUserId,
          shift_date: new Date(2098, 4, day, 12, 0, 0),
          leave_type: "work" as const,
          arrival_time: new Date(2098, 4, day, 6 + s, 0, 0),
          departure_time: new Date(2098, 4, day, 10 + s, 0, 0),
          notes: "attendance_wire_test bulk month",
        });
      }
    }
    await prisma.attendance.createMany({ data: rows });

    const res = await app.inject({
      method: "GET",
      url: "/api/admin/attendance?year=2098&month=5&limit=2000",
      headers: { Authorization: `Bearer ${adminToken}` },
    });

    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.success).toBe(true);
    expect(body.data).toHaveLength(120);
    expect(body.pagination.total).toBe(120);
  });
});

describe("GET /api/admin/attendance (month window boundaries, @db.Date)", () => {
  it("includes the month's own LAST day and excludes the neighbor month's first day", async () => {
    // shift_date is @db.Date — Prisma compares the filter Dates by their UTC
    // date part. The old LOCAL-midnight month bounds (22:00/23:00 UTC of the
    // previous day) shifted the whole window a day back: the June list
    // included May 31 and DROPPED June 30. Fixture rows sit exactly on the
    // 2098-06 / 2098-07 boundary.
    const juneLast = await prisma.attendance.create({
      data: {
        user_id: adminUserId,
        shift_date: new Date(2098, 5, 30, 12, 0, 0), // 2098-06-30, local noon
        leave_type: "work",
        arrival_time: new Date(2098, 5, 30, 8, 0, 0),
        departure_time: new Date(2098, 5, 30, 16, 0, 0),
        notes: "attendance_wire_test june-last-day",
      },
    });
    const julyFirst = await prisma.attendance.create({
      data: {
        user_id: adminUserId,
        shift_date: new Date(2098, 6, 1, 12, 0, 0), // 2098-07-01, local noon
        leave_type: "work",
        arrival_time: new Date(2098, 6, 1, 8, 0, 0),
        departure_time: new Date(2098, 6, 1, 16, 0, 0),
        notes: "attendance_wire_test july-first-day",
      },
    });

    const june = await app.inject({
      method: "GET",
      url: "/api/admin/attendance?year=2098&month=6&limit=100",
      headers: { Authorization: `Bearer ${adminToken}` },
    });
    expect(june.statusCode).toBe(200);
    const juneIds = june.json().data.map((r: { id: number }) => r.id);
    expect(juneIds).toContain(juneLast.id);
    expect(juneIds).not.toContain(julyFirst.id);

    const july = await app.inject({
      method: "GET",
      url: "/api/admin/attendance?year=2098&month=7&limit=100",
      headers: { Authorization: `Bearer ${adminToken}` },
    });
    expect(july.statusCode).toBe(200);
    const julyIds = july.json().data.map((r: { id: number }) => r.id);
    expect(julyIds).toContain(julyFirst.id);
    expect(julyIds).not.toContain(juneLast.id);
  });
});

describe("POST /api/admin/attendance (same-day duplicate window, @db.Date)", () => {
  it("rejects a second leave record on the SAME day", async () => {
    const first = await authPost("/api/admin/attendance", adminToken, {
      user_id: adminUserId,
      shift_date: "2098-08-10",
      leave_type: "vacation",
      leave_hours: 8,
      notes: "attendance_wire_test dup-leave-first",
    });
    expect(first.statusCode).toBe(201);

    // Old bug: the duplicate/overlap window was built from LOCAL midnights of
    // the UTC-midnight shiftDate, so the validation queried ONLY the previous
    // day — a same-day duplicate leave sailed through undetected.
    const second = await authPost("/api/admin/attendance", adminToken, {
      user_id: adminUserId,
      shift_date: "2098-08-10",
      leave_type: "sick",
      leave_hours: 8,
      notes: "attendance_wire_test dup-leave-second",
    });
    expect(second.statusCode).toBe(400);
    expect(second.json().success).toBe(false);
  });

  it("does NOT falsely reject a leave on the day AFTER an existing record", async () => {
    const first = await authPost("/api/admin/attendance", adminToken, {
      user_id: adminUserId,
      shift_date: "2098-08-10",
      leave_type: "vacation",
      leave_hours: 8,
      notes: "attendance_wire_test neighbor-day-first",
    });
    expect(first.statusCode).toBe(201);

    // Old bug (the other half): creating a record for the NEXT day queried
    // the window [Aug 10, Aug 11) — i.e. yesterday's records — and bounced
    // with a false "záznam o nepřítomnosti již existuje".
    const nextDay = await authPost("/api/admin/attendance", adminToken, {
      user_id: adminUserId,
      shift_date: "2098-08-11",
      leave_type: "vacation",
      leave_hours: 8,
      notes: "attendance_wire_test neighbor-day-next",
    });
    expect(nextDay.statusCode).toBe(201);
    expect(nextDay.json().success).toBe(true);
  });
});

describe("PUT /api/admin/attendance/:id (combined datetimes)", () => {
  it("updates a record with combined datetimes (incl. overnight departure)", async () => {
    const created = await prisma.attendance.create({
      data: {
        user_id: adminUserId,
        shift_date: new Date(2098, 2, 14, 12, 0, 0),
        leave_type: "work",
        arrival_time: new Date("2098-03-14T08:00:00"),
        departure_time: new Date("2098-03-14T16:30:00"),
        notes: "attendance_wire_test update",
      },
    });

    const res = await authPut(
      `/api/admin/attendance/${created.id}`,
      adminToken,
      {
        leave_type: "work",
        arrival_time: "2098-03-14T22:00:00",
        departure_time: "2098-03-15T06:00:00",
        break_start: "2098-03-15T02:00:00",
        break_end: "2098-03-15T02:30:00",
        notes: "attendance_wire_test updated",
      },
    );

    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.success).toBe(true);

    const rec = await prisma.attendance.findUnique({
      where: { id: created.id },
    });
    expect(rec!.arrival_time?.getTime()).toBe(
      new Date("2098-03-14T22:00:00").getTime(),
    );
    expect(rec!.departure_time?.getTime()).toBe(
      new Date("2098-03-15T06:00:00").getTime(),
    );
    expect(rec!.break_start?.getTime()).toBe(
      new Date("2098-03-15T02:00:00").getTime(),
    );
    expect(rec!.break_end?.getTime()).toBe(
      new Date("2098-03-15T02:30:00").getTime(),
    );
    expect(rec!.notes).toBe("attendance_wire_test updated");
  });

  it("clears times when null is sent (leave-type edit)", async () => {
    const created = await prisma.attendance.create({
      data: {
        user_id: adminUserId,
        shift_date: new Date(2098, 2, 16, 12, 0, 0),
        leave_type: "work",
        arrival_time: new Date("2098-03-16T08:00:00"),
        departure_time: new Date("2098-03-16T16:30:00"),
        notes: "attendance_wire_test to-leave",
      },
    });

    const res = await authPut(
      `/api/admin/attendance/${created.id}`,
      adminToken,
      {
        leave_type: "vacation",
        leave_hours: 8,
        arrival_time: null,
        departure_time: null,
        break_start: null,
        break_end: null,
        notes: "attendance_wire_test to-leave",
      },
    );

    expect(res.statusCode).toBe(200);
    expect(res.json().success).toBe(true);

    const rec = await prisma.attendance.findUnique({
      where: { id: created.id },
    });
    expect(rec!.leave_type).toBe("vacation");
    expect(rec!.arrival_time).toBeNull();
    expect(rec!.departure_time).toBeNull();
  });
});
