import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { AuthUser } from '@alqove/types';
import type { Membership, Timesheet } from '@alqove/api-client';
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { useAuthStore } from '@/stores/auth';
import { TimesheetsClient } from '../timesheets-client';

const { myMemberships, list, detail, punches, update, remove, create } = vi.hoisted(() => ({
  myMemberships: vi.fn(), list: vi.fn(), detail: vi.fn(), punches: vi.fn(),
  update: vi.fn(), remove: vi.fn(), create: vi.fn(),
}));
vi.mock('@/lib/api', () => ({ api: {
  team: { myMemberships },
  timekeeping: { timesheets: { list, detail }, punches: { list: punches, update, remove, create } },
} }));
const member: Membership = { id: 'm1', store_id: 's1', role: 'manager', is_exempt: false, capabilities: ['timesheets.approve', 'punches.manage'] };
const sheet: Timesheet = {
  id: 't1', store_membership_id: 'm1', member_name: 'Ada', week_start_date: '2026-09-07',
  scheduled_minutes: 480, regular_minutes: 480, overtime_minutes: 0, doubletime_minutes: 0,
  unpaid_break_minutes: 0, status: 'pending', calculation_metadata: { timezone: 'UTC' },
};
const punch = { id: 'p1', punch_type: 'clock_in', punched_at: '2026-09-07T09:00:00Z', revision: 'opaque-v1', note: null, break_type: null };
const detailKey = ['timekeeping', 's1', 'timesheet', 't1'];
let qc: QueryClient;
beforeEach(async () => {
  vi.resetAllMocks();
  useAuthStore.getState().clearAuth();
  localStorage.clear();
  useAuthStore.getState().setAuth({ id: 'u1', email: 'fixture@local.test' } as AuthUser, 'fixture-token');
  myMemberships.mockResolvedValue({ data: [member] });
  await useAuthStore.getState().refreshMemberships();
  list.mockResolvedValue({ data: [sheet] });
  detail.mockResolvedValue({ data: sheet });
  punches.mockResolvedValue({ data: [punch] });
  update.mockResolvedValue({ data: punch });
  remove.mockResolvedValue({ data: { deleted: true } });
  create.mockResolvedValue({ data: punch });
  qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
});
afterEach(() => { cleanup(); qc.clear(); });

const corrections = [
  { action: 'edit', open: 'Edit punch', note: 'Correction note', submit: 'Save correction' },
  { action: 'delete', open: 'Delete punch', note: 'Deletion note', submit: 'Confirm deletion' },
  { action: 'add', open: 'Add punch', note: 'Correction note', submit: 'Save correction' },
] as const;
async function openCorrection(correction: typeof corrections[number]) {
  render(<QueryClientProvider client={qc}><TimesheetsClient /></QueryClientProvider>);
  fireEvent.click(await screen.findByRole('button', { name: 'Ada' }));
  fireEvent.click(await screen.findByRole('button', { name: correction.open }));
  fireEvent.change(screen.getByLabelText(correction.note), { target: { value: 'Reviewed correction' } });
  return screen.getByRole('button', { name: correction.submit });
}

it.each(corrections.flatMap(correction => (['approved', 'exported', 'missing'] as const).map(status => ({ ...correction, status }))))(
  'blocks $action dispatch if cache becomes $status before React renders it', async correction => {
    const submit = await openCorrection(correction);
    await act(async () => {
      // Query cache writes synchronously; observer notifications/render are batched.
      if (correction.status === 'missing') qc.removeQueries({ queryKey: detailKey, exact: true });
      else {
        const latest = { data: { ...sheet, status: correction.status } };
        detail.mockResolvedValue(latest);
        qc.setQueryData(detailKey, latest);
      }
      expect(submit).toBeEnabled();
      fireEvent.click(submit);
    });
    expect(update).not.toHaveBeenCalled();
    expect(remove).not.toHaveBeenCalled();
    expect(create).not.toHaveBeenCalled();
  },
);

it.each(corrections.flatMap(correction => (['approved', 'exported'] as const).map(status => ({ ...correction, status }))))(
  'closes retained $action correction when pending becomes $status in the detail cache', async correction => {
    expect(await openCorrection(correction)).toBeEnabled();
    await act(async () => {
      qc.setQueryData(detailKey, { data: { ...sheet, status: correction.status } });
    });
    expect(await screen.findByText(`${correction.status} · All punch times in UTC`)).toBeInTheDocument();
    expect(screen.queryByRole('button', { name: correction.submit })).not.toBeInTheDocument();
    expect(screen.queryByLabelText(correction.note)).not.toBeInTheDocument();
    expect(update).not.toHaveBeenCalled();
    expect(remove).not.toHaveBeenCalled();
    expect(create).not.toHaveBeenCalled();
  },
);
