import os from "node:os";
import path from "node:path";
import fs from "node:fs";

// Env + a throwaway DB must be in place before ../db is first imported (it
// creates the schema on import), so this runs ahead of the other imports.
const dbDir = fs.mkdtempSync(path.join(os.tmpdir(), "eatme-payroll-test-"));
process.env.DB_PATH = path.join(dbDir, "test.db");
process.env.GOOGLE_CLIENT_ID = "test-client-id";
process.env.ADMIN_EMAILS = "admin@example.com";
process.env.JWT_SECRET = "test-secret";

import { db } from "../db";
import { setRate } from "./payRates";
import { confirmClosure, findClosure, getClosureSummary } from "./closure";
import {
  findPayroll,
  getPeriodOverview,
  lockAndFinalizePayroll,
  markPaid,
  reopenPayroll,
  saveDraftAdjustments,
} from "./payroll";

const PERIOD = "2026-06";

function makeEmployee(email: string): number {
  const result = db.prepare("INSERT INTO employees (email, name) VALUES (?, ?)").run(email, email);
  return Number(result.lastInsertRowid);
}

/** created_at needs to predate the shifts in these tests, to simulate "employee existed before the rate was ever set". */
function makeEmployeeWithCreatedAt(email: string, createdAt: string): number {
  const result = db
    .prepare("INSERT INTO employees (email, name, created_at) VALUES (?, ?, ?)")
    .run(email, email, createdAt);
  return Number(result.lastInsertRowid);
}

function insertEvent(employeeId: number, type: "clock_in" | "clock_out", ts: string): void {
  db.prepare("INSERT INTO attendance_events (employee_id, type, ts) VALUES (?, ?, ?)").run(employeeId, type, ts);
}

describe("closure + payroll workflow", () => {
  test("summary is computed from worked sessions using the rate active on the shift's date", () => {
    const eve = makeEmployee("eve@example.com");
    setRate(eve, 200, "2026-01-01");

    insertEvent(eve, "clock_in", "2026-06-05T08:00:00.000Z");
    insertEvent(eve, "clock_out", "2026-06-05T12:00:00.000Z"); // 4h shift

    const summary = getClosureSummary(eve, PERIOD);
    expect(summary.worked_minutes).toBe(240);
    expect(summary.shift_count).toBe(1);
    expect(summary.earned_estimate).toBe(800); // 4h * 200 Kč
  });

  test("employee cannot confirm the month while a shift is still open, and confirming is idempotent", () => {
    // Uses the real current month/time (not the fixed PERIOD) because a genuinely "still open"
    // shift only makes sense within the last 12h — anything older gets auto-closed (see
    // attendance.test.ts), which would make this scenario impossible to set up otherwise.
    const currentPeriod = new Date().toISOString().slice(0, 7);
    const frank = makeEmployee("frank@example.com");
    setRate(frank, 150, "2000-01-01");
    insertEvent(frank, "clock_in", new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString()); // still open

    expect(() => confirmClosure(frank, currentPeriod)).toThrow("otevřenou směnu");

    insertEvent(frank, "clock_out", new Date().toISOString());
    confirmClosure(frank, currentPeriod);
    expect(findClosure(frank, currentPeriod)!.status).toBe("confirmed");

    confirmClosure(frank, currentPeriod); // already confirmed -> no-op, not an error
    expect(findClosure(frank, currentPeriod)!.status).toBe("confirmed");
  });

  test("admin can only adjust tips/bonus/other after employee confirmation, and locking freezes the numbers", () => {
    const gina = makeEmployee("gina@example.com");
    setRate(gina, 200, "2026-01-01");
    insertEvent(gina, "clock_in", "2026-06-01T08:00:00.000Z");
    insertEvent(gina, "clock_out", "2026-06-01T16:00:00.000Z"); // 8h -> base 1600

    expect(() =>
      saveDraftAdjustments(gina, PERIOD, { tips_amount: 100, bonus_amount: 0, other_amount: 0 })
    ).toThrow("po potvrzení docházky");

    confirmClosure(gina, PERIOD);

    const draft = saveDraftAdjustments(gina, PERIOD, { tips_amount: 300, bonus_amount: 200, other_amount: -50 });
    expect(draft.status).toBe("draft");
    expect(draft.final_amount).toBe(1600 + 300 + 200 - 50);

    const locked = lockAndFinalizePayroll(gina, PERIOD);
    expect(locked.status).toBe("ready");
    expect(locked.final_amount).toBe(draft.final_amount);
    expect(findClosure(gina, PERIOD)!.status).toBe("locked");

    expect(() =>
      saveDraftAdjustments(gina, PERIOD, { tips_amount: 999, bonus_amount: 0, other_amount: 0 })
    ).toThrow("uzamčen");
    expect(() => lockAndFinalizePayroll(gina, PERIOD)).toThrow("nepotvrdil zaměstnanec");

    const paid = markPaid(gina, PERIOD);
    expect(paid.status).toBe("paid");
    expect(paid.payment_date).toBeTruthy();

    expect(() => markPaid(gina, PERIOD)).toThrow("nejdřív uzamčena");
  });

  test("the first rate set for an employee applies retroactively; later raises only apply from today", () => {
    const iris = makeEmployeeWithCreatedAt("iris@example.com", "2026-05-01T00:00:00.000Z");
    insertEvent(iris, "clock_in", "2026-06-03T08:00:00.000Z");
    insertEvent(iris, "clock_out", "2026-06-03T12:00:00.000Z"); // 4h shift, before any rate exists

    expect(getClosureSummary(iris, PERIOD).earned_estimate).toBe(0);

    setRate(iris, 200); // no valid_from given -> must cover the already-worked June shift
    expect(getClosureSummary(iris, PERIOD).earned_estimate).toBe(800); // 4h * 200

    setRate(iris, 300); // a raise -> only from today, not retroactive
    expect(getClosureSummary(iris, PERIOD).earned_estimate).toBe(800); // June shift unaffected
  });

  test("reopen undoes a premature lock so the numbers can be corrected, but not once already paid", () => {
    const jack = makeEmployeeWithCreatedAt("jack@example.com", "2026-05-01T00:00:00.000Z");
    insertEvent(jack, "clock_in", "2026-06-04T08:00:00.000Z");
    insertEvent(jack, "clock_out", "2026-06-04T12:00:00.000Z"); // 4h shift

    confirmClosure(jack, PERIOD);
    const locked = lockAndFinalizePayroll(jack, PERIOD); // locked before any rate was set
    expect(locked.base_amount).toBe(0);

    reopenPayroll(jack, PERIOD);
    expect(findClosure(jack, PERIOD)!.status).toBe("confirmed");
    expect(findPayroll(jack, PERIOD)!.status).toBe("draft");

    setRate(jack, 150);
    const relocked = lockAndFinalizePayroll(jack, PERIOD);
    expect(relocked.base_amount).toBe(600); // 4h * 150, now correct

    markPaid(jack, PERIOD);
    expect(() => reopenPayroll(jack, PERIOD)).toThrow("Lze odemknout");
  });

  test("admin overview shows live status for employees who haven't touched their closure yet", () => {
    const henry = makeEmployee("henry@example.com");
    setRate(henry, 100, "2026-01-01");
    insertEvent(henry, "clock_in", "2026-06-02T08:00:00.000Z");
    insertEvent(henry, "clock_out", "2026-06-02T10:00:00.000Z"); // 2h -> base 200

    const row = getPeriodOverview(PERIOD).find((r) => r.employee_id === henry)!;
    expect(row.closure_status).toBe("waiting_employee");
    expect(row.payroll_status).toBeNull();
    expect(row.worked_minutes).toBe(120);
    expect(row.base_amount).toBe(200);
  });
});
