import { test, expect, fixture, api, login, dbRows, evidence, API, storePath, runDir } from './helpers';
import fs from 'node:fs';
import path from 'node:path';
import { createHash } from 'node:crypto';

// Isolated synthetic clock sessions use real server time, never browser date mocks.
test('staff clocks and breaks through real buttons with durable idempotent replay', async ({ page, request }) => {
  await api(request, 'owner', 'PATCH', '/schedule/settings', { clock_in_early_minutes: 120, clock_in_late_minutes: 120, clock_out_late_minutes: 120 });
  const initial = await api(request, 'staff', 'GET', '/my/clock');
  const now = new Date(initial.server_time);
  const startsAt = new Date(now.getTime() - 60_000).toISOString();
  const endsAt = new Date(now.getTime() + 60 * 60_000).toISOString();
  const shift = await api(request, 'owner', 'POST', '/schedule/shifts', {
    store_membership_id: fixture.actors.staff.memberships.a, starts_at: startsAt, ends_at: endsAt, notes: fixture.marker + ' live clock',
  }, 201);
  const label = new Intl.DateTimeFormat('en-CA', { timeZone: 'America/Los_Angeles' }).format(now);
  const week = await api(request, 'owner', 'GET', `/schedule/week?week_start=${label}`);
  await api(request, 'owner', 'POST', '/schedule/publish', { week_start: week.week_start });
  let firstClockRequest: { data: unknown; key: string } | undefined;
  page.on('request', r => {
    if (!firstClockRequest && r.method() === 'POST' && r.url().endsWith('/my/clock')) {
      firstClockRequest = { data: r.postDataJSON(), key: r.headers()['idempotency-key'] };
    }
  });
  await login(page, 'staff', '/staff/clock');
  await expect(page.getByRole('button', { name: 'Clock in', exact: true })).toBeEnabled();
  let lastSecond = Math.floor(now.getTime() / 1000);
  async function nextSecond() {
    await expect.poll(async () => {
      const clock = await api(request, 'staff', 'GET', '/my/clock');
      const second = Math.floor(new Date(clock.server_time).getTime() / 1000);
      if (second > lastSecond) { lastSecond = second; return true; }
      return false;
    }, { intervals: [150, 250, 500] }).toBe(true);
  }
  for (const [button, status] of [['Clock in', 'clocked_in'], ['Start break', 'on_break'], ['End break', 'clocked_in'], ['Clock out', 'clocked_out']]) {
    await nextSecond();
    await page.getByRole('button', { name: button, exact: true }).click();
    await expect.poll(async () => (await api(request, 'staff', 'GET', '/my/clock')).status).toBe(status);
  }
  expect(firstClockRequest?.key).toBeTruthy();
  const replayedClock = await api(request, 'staff', 'POST', '/my/clock', firstClockRequest!.data, 200, 'a', firstClockRequest!.key);
  expect(replayedClock.status).toBe('clocked_in');
  const clock = await api(request, 'staff', 'GET', '/my/clock');
  expect(clock.status).toBe('clocked_out');
  expect(clock.punches.map((p: any) => p.punch_type).sort()).toEqual(['break_end', 'break_start', 'clock_in', 'clock_out']);
  expect(dbRows('time_punches').filter((p: any) => !p.deleted_at)).toHaveLength(4);
  const clockIn = clock.punches.find((p: any) => p.punch_type === 'clock_in');
  await evidence('clock-real-buttons', { shift, clock, sql: dbRows('time_punches') });

});

test('manager correction and approval leads to owner browser CSV download and immutable replay', async ({ request, browser }) => {
  const week = { week_start: '2026-05-04' }, label = '2026-05-04';
  const startsAt = '2026-05-04T16:00:00Z';
  const clockIn = await api(request, 'manager', 'POST', '/punches', {
    store_membership_id: fixture.actors.manager.memberships.a,
    punch_type: 'clock_in', punched_at: startsAt, note: fixture.marker + ' correction setup',
  }, 201);
  await api(request, 'manager', 'POST', '/punches', {
    store_membership_id: fixture.actors.manager.memberships.a,
    punch_type: 'clock_out', punched_at: '2026-05-04T17:00:00Z', note: fixture.marker + ' correction setup',
  }, 201);

  const context = await browser.newContext({ timezoneId: 'America/Los_Angeles' });
  const manager = await context.newPage();
  try {
    await login(manager, 'manager', '/seller/timesheets');
    await manager.getByLabel('Week containing').fill(week.week_start);
    await manager.getByRole('button', { name: 'Recalculate week', exact: true }).click();
    await manager.getByRole('button', { name: fixture.actors.manager.name, exact: true }).click();
    const detail = manager.getByRole('region', { name: 'Timesheet detail' });
    const row = detail.getByRole('row').filter({ has: manager.getByRole('cell', { name: 'clock in', exact: true }) });
    await row.getByRole('button', { name: 'Edit punch', exact: true }).click();
    const corrected = new Date(new Date(clockIn.punched_at).getTime() - 60 * 60_000);
    const parts = new Intl.DateTimeFormat('sv-SE', { timeZone: 'America/Los_Angeles', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).format(corrected).replace(' ', 'T');
    await manager.getByLabel(/Punch time/).fill(parts);
    await manager.getByLabel('Correction note', { exact: true }).fill(fixture.marker + ' correct missed first hour');
    await manager.getByRole('button', { name: 'Save correction', exact: true }).click();
    await expect(manager.getByLabel('Correction note', { exact: true })).toHaveCount(0);
    let sheets = await api(request, 'manager', 'GET', `/timesheets?week_start=${week.week_start}`);
    let sheet = sheets.find((t: any) => t.store_membership_id === fixture.actors.manager.memberships.a);
    expect(sheet).not.toHaveProperty('total_pay_cents');
    await manager.getByRole('button', { name: 'Recalculate week', exact: true }).click();
    await expect.poll(async () => {
      sheets = await api(request, 'manager', 'GET', `/timesheets?week_start=${week.week_start}`);
      sheet = sheets.find((t: any) => t.store_membership_id === fixture.actors.manager.memberships.a);
      return sheet.regular_minutes;
    }).toBe(120);
    await api(request, 'manager', 'GET', '/payroll/exports', undefined, 403);
    await api(request, 'owner', 'GET', `/timesheets/${sheet.id}`, undefined, 404, 'b');
    await manager.getByRole('button', { name: 'Approve timesheet', exact: true }).click();
    await expect(manager.getByRole('button', { name: 'Unlock timesheet', exact: true })).toBeVisible();
    sheet = await api(request, 'owner', 'GET', `/timesheets/${sheet.id}`);
    expect(sheet.status).toBe('approved');
    expect(sheet.total_pay_cents).toBe(sheet.regular_minutes * 40);
    // Zero-hour coworker sheets are still real rows and must also be approved.
    for (const other of sheets.filter((t: any) => t.id !== sheet.id)) await api(request, 'manager', 'POST', `/timesheets/${other.id}/approve`, {});
    await evidence('manager-correction-approval', { sheet, sql: dbRows('timesheets'), punches: dbRows('time_punches') });

    const ownerContext = await browser.newContext();
    const owner = await ownerContext.newPage();
    try {
      await login(owner, 'owner', '/seller/payroll');
      await owner.getByLabel(/First week|Start week|Week starting|Week start/i).first().fill(week.week_start);
      // Export controls are finalized with the production UI; no API substitute for this action.
      const lastWeek = owner.getByLabel(/Last week|End week/i);
      if (await lastWeek.count()) await lastWeek.fill(week.week_start);
      const exportRequest = owner.waitForRequest(r => r.method() === 'POST' && r.url().endsWith('/payroll/exports'));
      await owner.getByRole('button', { name: 'Create CSV export', exact: true }).click();
      const sent = await exportRequest;
      const key = sent.headers()['idempotency-key'];
      expect(key).toBeTruthy();
      const body = sent.postDataJSON();
      const downloadPromise = owner.waitForEvent('download');
      await owner.getByRole('button', { name: /Download/i }).first().click();
      const download = await downloadPromise;
      const file = path.join(runDir, 'payroll-download.csv');
      await download.saveAs(file);
      fs.chmodSync(file, 0o600);
      const csv = fs.readFileSync(file);
      expect(csv.toString()).toContain(fixture.actors.manager.name);
      const history = await api(request, 'owner', 'GET', '/payroll/exports');
      expect(history).toHaveLength(1);
      const replay = await api(request, 'owner', 'POST', '/payroll/exports', body, 200, 'a', key);
      expect(replay.id).toBe(history[0].id);
      const response = await request.get(API + storePath(`/payroll/exports/${replay.id}/download`), { headers: { Authorization: ['Bearer', fixture.actors.owner.token].join(' ') } });
      expect(response.status()).toBe(200);
      expect(await response.body()).toEqual(csv);
      await api(request, 'owner', 'POST', '/payroll/exports', { week_starts: ['2026-01-05'] }, 409, 'a', key);
      await api(request, 'manager', 'POST', `/timesheets/${sheet.id}/unlock`, {}, 409);
      const freshPunch = (await api(request, 'manager', 'GET', `/punches?store_membership_id=${fixture.actors.manager.memberships.a}&from=${week.week_start}&to=${label}`)).find((p: any) => p.id === clockIn.id);
      await api(request, 'manager', 'PATCH', `/punches/${clockIn.id}`, { revision: freshPunch.revision, note: 'must not mutate exported history', punched_at: startsAt }, 409);
      expect(dbRows('payroll_exports')).toHaveLength(1);
      await evidence('payroll-browser-download-replay', { export: replay, csv_sha256: createHash('sha256').update(csv).digest('hex'), csv_bytes: csv.length, sql: dbRows('payroll_exports') });
    } finally { await ownerContext.close(); }
  } finally { await context.close(); }
});
