import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { vi, it, expect, beforeEach } from 'vitest';
import { HoursClient } from '../../../../(staff)/staff/hours/hours-client';
import { TimesheetsClient } from '../timesheets-client';
const auth = vi.hoisted(() => ({ commandGeneration: 1, user: { id: 'u1' }, token: 'token', selectedStoreId: 's1', memberships: [{ store_id: 's1' }], isLoading: false }));
vi.mock('@/stores/auth', () => ({ useAuthStore: { getState: () => auth } }));
const list = vi.fn(); const own = vi.fn(); const detail = vi.fn(); const update = vi.fn(); const punches = vi.fn();
const approve = vi.fn(); const unlock = vi.fn(); const create = vi.fn(); const remove = vi.fn(); const recalculate = vi.fn();
vi.mock('@/lib/api', () => ({ api: { timekeeping: { timesheets: { recalculate: (...a: unknown[]) => recalculate(...a), approve: (...a: unknown[]) => approve(...a), unlock: (...a: unknown[]) => unlock(...a), list: (...a: unknown[]) => list(...a), own: (...a: unknown[]) => own(...a), detail: (...a: unknown[]) => detail(...a) }, punches: { create: (...a: unknown[]) => create(...a), remove: (...a: unknown[]) => remove(...a), list: (...a: unknown[]) => punches(...a), update: (...a: unknown[]) => update(...a) } } } }));
vi.mock('@/lib/stores/store-context', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/lib/stores/store-context')>()), requireSelectedStore: (s: string) => s, useSelectedMembership: () => ({ store_id: 's1', capabilities: ['timesheets.approve', 'punches.manage'] }), useSelectedStoreId: () => 's1' }));
const sheet = { id: 't1', store_membership_id: 'm1', member_name: 'Ada', week_start_date: '2026-09-07', scheduled_minutes: 480, regular_minutes: 450, overtime_minutes: 30, doubletime_minutes: 0, unpaid_break_minutes: 30, total_pay_cents: 18500, status: 'pending', calculation_metadata: {} };
function wrap(child: React.ReactNode) { return <QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>{child}</QueryClientProvider>; }
beforeEach(() => { vi.clearAllMocks(); auth.selectedStoreId = 's1'; list.mockResolvedValue({ data: [sheet] }); own.mockResolvedValue({ data: [sheet] }); });
it('refreshes open detail approval guards after recalculating the selected week', async () => {
  detail.mockResolvedValue({ data: sheet }); punches.mockResolvedValue({ data: [] });
  recalculate.mockImplementation(async () => { detail.mockResolvedValue({ data: { ...sheet, calculation_metadata: { open_sessions: [{ punch_id: 'p1' }] } } }); return { data: [sheet] }; });
  render(wrap(<TimesheetsClient />));
  fireEvent.change(screen.getByLabelText('Week containing'), { target: { value: '2026-09-07' } });
  fireEvent.click(await screen.findByRole('button', { name: /Ada/ }));
  expect(await screen.findByRole('button', { name: 'Approve timesheet' })).toBeEnabled();
  fireEvent.click(screen.getByRole('button', { name: 'Recalculate week' }));
  await waitFor(() => expect(screen.getByRole('button', { name: 'Approve timesheet' })).toBeDisabled());
  expect(recalculate).toHaveBeenCalledWith('s1', '2026-09-07');
});
it('does not dispatch a correction from a stale store form', async () => {
  detail.mockResolvedValue({ data: sheet });
  punches.mockResolvedValue({ data: [{ id: 'p1', punch_type: 'clock_in', punched_at: '2026-09-07T09:00:00Z', revision: 'r1' }] });
  render(wrap(<TimesheetsClient />));
  fireEvent.click(await screen.findByRole('button', { name: /Ada/ }));
  fireEvent.click(await screen.findByRole('button', { name: 'Edit punch' }));
  fireEvent.change(screen.getByLabelText('Correction note'), { target: { value: 'Correction' } });
  auth.selectedStoreId = 's2';
  fireEvent.click(screen.getByRole('button', { name: 'Save correction' }));
  await new Promise(r => setTimeout(r, 10));
  expect(update).not.toHaveBeenCalled();
});
it('hard-blocks currently open sessions even when they are not yet stranded', async () => {
  detail.mockResolvedValue({ data: { ...sheet, calculation_metadata: { open_sessions: [{ punch_id: 'p1' }] } } }); punches.mockResolvedValue({ data: [] });
  render(wrap(<TimesheetsClient />));
  fireEvent.click(await screen.findByRole('button', { name: /Ada/ }));
  expect(await screen.findByRole('button', { name: 'Approve timesheet' })).toBeDisabled();
});
it('acknowledges oversized approval, refetches approved state and unlocks', async () => {
  const pending = { ...sheet, calculation_metadata: { oversized_sessions: [{ minutes: 1080 }] } };
  detail.mockResolvedValue({ data: pending }); punches.mockResolvedValue({ data: [] });
  approve.mockImplementation(async () => { detail.mockResolvedValue({ data: { ...pending, status: 'approved' } }); return { data: {} }; });
  unlock.mockResolvedValue({ data: {} });
  render(wrap(<TimesheetsClient />));
  fireEvent.click(await screen.findByRole('button', { name: /Ada/ }));
  expect(await screen.findByRole('button', { name: 'Approve timesheet' })).toBeDisabled();
  fireEvent.click(screen.getByRole('checkbox', { name: /reviewed.*oversized/ }));
  fireEvent.click(screen.getByRole('button', { name: 'Approve timesheet' }));
  fireEvent.click(await screen.findByRole('button', { name: 'Unlock timesheet' }));
  await waitFor(() => expect(unlock).toHaveBeenCalledWith('s1', 't1'));
  expect(approve).toHaveBeenCalledWith('s1', 't1', true);
});
it('adds and deletes punches only with a correction note and deletion revision', async () => {
  detail.mockResolvedValue({ data: sheet });
  const punch = { id: 'p1', punch_type: 'clock_in', punched_at: '2026-09-07T09:00:00Z', revision: 'r1' };
  punches.mockResolvedValue({ data: [punch] }); create.mockResolvedValue({ data: punch }); remove.mockResolvedValue({ data: { deleted: true } });
  render(wrap(<TimesheetsClient />)); fireEvent.click(await screen.findByRole('button', { name: /Ada/ }));
  fireEvent.click(await screen.findByRole('button', { name: 'Add punch' }));
  expect(screen.getByRole('button', { name: 'Save correction' })).toBeDisabled();
  fireEvent.change(screen.getByLabelText('Correction note'), { target: { value: 'Missed punch' } });
  fireEvent.click(screen.getByRole('button', { name: 'Save correction' }));
  await waitFor(() => expect(create).toHaveBeenCalledWith('s1', expect.objectContaining({ store_membership_id: 'm1', note: 'Missed punch', punch_type: 'clock_in' })));
  await waitFor(() => expect(screen.getByRole('button', { name: 'Delete punch' })).toBeEnabled());
  fireEvent.click(screen.getByRole('button', { name: 'Delete punch' }));
  fireEvent.change(screen.getByLabelText('Deletion note'), { target: { value: 'Duplicate entry' } });
  fireEvent.click(screen.getByRole('button', { name: 'Confirm deletion' }));
  await waitFor(() => expect(remove).toHaveBeenCalledWith('s1', 'p1', { revision: 'r1', note: 'Duplicate entry' }));
});
it('preserves paid break-end classification when editing only its note', async () => {
  detail.mockResolvedValue({ data: sheet });
  punches.mockResolvedValue({ data: [{ id: 'p1', punch_type: 'break_end', break_type: 'paid', punched_at: '2026-09-07T09:30:00Z', revision: 'r1' }] });
  update.mockResolvedValue({ data: {} });
  render(wrap(<TimesheetsClient />)); fireEvent.click(await screen.findByRole('button', { name: /Ada/ }));
  fireEvent.click(await screen.findByRole('button', { name: 'Edit punch' }));
  fireEvent.change(screen.getByLabelText('Correction note'), { target: { value: 'Reviewed break' } });
  fireEvent.click(screen.getByRole('button', { name: 'Save correction' }));
  await waitFor(() => expect(update).toHaveBeenCalledWith('s1', 'p1', expect.objectContaining({ break_type: 'paid' })));
});
it('shows own hours/pay read only', async () => {
  render(wrap(<HoursClient />));
  expect(await screen.findByText('$185.00')).toBeInTheDocument();
  expect(screen.queryByRole('button', { name: /approve|edit|recalculate/i })).not.toBeInTheDocument();
  expect(own).toHaveBeenCalledWith('s1');
});
it('manager detail never renders money and sends correction note plus original revision', async () => {
  const punch = { id: 'p1', store_membership_id: 'm1', punch_type: 'clock_in', punched_at: '2026-09-07T09:00:00Z', revision: 'opaque-v1', note: null, break_type: null };
  detail.mockResolvedValue({ data: sheet }); punches.mockResolvedValue({ data: [punch] }); update.mockResolvedValue({ data: punch });
  render(wrap(<TimesheetsClient />));
  fireEvent.click(await screen.findByRole('button', { name: /Ada/ }));
  fireEvent.click(await screen.findByRole('button', { name: 'Edit punch' }));
  fireEvent.change(screen.getByLabelText('Correction note'), { target: { value: 'Corrected missed start' } });
  fireEvent.click(screen.getByRole('button', { name: 'Save correction' }));
  await waitFor(() => expect(update).toHaveBeenCalledWith('s1', 'p1', expect.objectContaining({ revision: 'opaque-v1', note: 'Corrected missed start' })));
  expect(screen.queryByText('$185.00')).not.toBeInTheDocument();
});
