import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ReviewModal } from '../review-modal';

const createMock = vi.fn();
const updateMock = vi.fn();
const uploadMock = vi.fn();

vi.mock('@/lib/api', () => ({
  api: {
    reviews: {
      create: (...a: unknown[]) => createMock(...a),
      update: (...a: unknown[]) => updateMock(...a),
      uploadAttachment: (...a: unknown[]) => uploadMock(...a),
    },
  },
}));

function wrap(node: React.ReactNode) {
  const qc = new QueryClient({
    defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
  });
  return <QueryClientProvider client={qc}>{node}</QueryClientProvider>;
}

const orderItem = {
  id: 'oi-1',
  title_snapshot: 'Vintage Tee',
  image_url_snapshot: null,
};

const baseReview = {
  id: 'rev-1',
  order_item_id: 'oi-1',
  order_id: 'ord-1',
  store_id: 's-1',
  reviewer_user_id: 'u-1',
  rating: 4,
  rating_item_as_described: 4,
  rating_shipping_speed: 5,
  rating_communication: 3,
  rating_packaging: 4,
  title: 'Nice',
  body: 'A perfectly fine purchase that I would recommend to a friend.',
  state: 'visible' as const,
  edited_at: null,
  hidden_by_admin_id: null,
  hidden_at: null,
  hide_reason: null,
  is_editable: true,
  photos: [],
  created_at: '2026-05-01T10:00:00Z',
  updated_at: '2026-05-01T10:00:00Z',
};

describe('ReviewModal', () => {
  beforeEach(() => {
    createMock.mockReset();
    updateMock.mockReset();
    uploadMock.mockReset();
  });

  it('renders title with store name and order item title', () => {
    render(
      wrap(
        <ReviewModal
          orderItem={orderItem}
          storeName="Revive"
          mode="create"
          onClose={() => {}}
        />,
      ),
    );
    expect(
      screen.getByText(/How was your experience with Revive\?/i),
    ).toBeInTheDocument();
    expect(screen.getByText('Vintage Tee')).toBeInTheDocument();
  });

  it('pre-fills dimensions when overall is set (touched-set: untouched dims follow)', () => {
    render(
      wrap(
        <ReviewModal
          orderItem={orderItem}
          storeName="Revive"
          mode="create"
          onClose={() => {}}
        />,
      ),
    );
    // 5 overall + 4 dims × 5 = 25 star buttons total.
    // Click "4 stars" on the overall picker (first one).
    const allFour = screen.getAllByRole('button', { name: '4 stars' });
    fireEvent.click(allFour[0]); // overall
    // All four dimension pickers should now be filled to 4 — meaning the 4th star of each is amber.
    // Confirm by checking the dimension picker labels' amber count.
    const groups = screen.getAllByRole('group');
    // groups[0] is overall, groups[1..4] are dimensions
    for (let i = 1; i < groups.length; i++) {
      const stars = groups[i].querySelectorAll('button');
      expect(stars[3].className).toMatch(/text-amber-500/); // 4th star filled
    }
  });

  it('once a dim is touched, changing overall does not overwrite it', () => {
    render(
      wrap(
        <ReviewModal
          orderItem={orderItem}
          storeName="Revive"
          mode="create"
          onClose={() => {}}
        />,
      ),
    );
    const groups = screen.getAllByRole('group');
    // Touch the first dimension to "2 stars".
    const dim1Buttons = groups[1].querySelectorAll('button');
    fireEvent.click(dim1Buttons[1]); // 2 stars
    // Now set overall to 5.
    const overallButtons = groups[0].querySelectorAll('button');
    fireEvent.click(overallButtons[4]); // 5 stars
    // The touched dim1 should still be at 2 — only the 2nd star is amber, not the 5th.
    const dim1AfterButtons = groups[1].querySelectorAll('button');
    expect(dim1AfterButtons[1].className).toMatch(/text-amber-500/);
    expect(dim1AfterButtons[4].className).not.toMatch(/text-amber-500/);
    // Other untouched dims should follow to 5.
    const dim2Buttons = groups[2].querySelectorAll('button');
    expect(dim2Buttons[4].className).toMatch(/text-amber-500/);
  });

  it('disables submit when body is shorter than 20 characters', () => {
    render(
      wrap(
        <ReviewModal
          orderItem={orderItem}
          storeName="Revive"
          mode="create"
          onClose={() => {}}
        />,
      ),
    );
    const overallButtons = screen.getAllByRole('group')[0].querySelectorAll('button');
    fireEvent.click(overallButtons[4]);
    fireEvent.change(screen.getByLabelText(/Review \*/), {
      target: { value: 'Too short.' },
    });
    expect(screen.getByRole('button', { name: /Submit review/i })).toBeDisabled();
    expect(screen.getByTestId('review-body-min-indicator')).toHaveTextContent(
      /more character/i,
    );
  });

  it('submits a create review with all fields when valid', async () => {
    createMock.mockResolvedValue({ data: baseReview });
    const onClose = vi.fn();
    render(
      wrap(
        <ReviewModal
          orderItem={orderItem}
          storeName="Revive"
          mode="create"
          onClose={onClose}
        />,
      ),
    );
    const groups = screen.getAllByRole('group');
    fireEvent.click(groups[0].querySelectorAll('button')[4]); // overall = 5
    fireEvent.change(screen.getByLabelText(/Title/i), {
      target: { value: 'Loved it' },
    });
    fireEvent.change(screen.getByLabelText(/Review \*/), {
      target: { value: 'This is a sufficiently long review of the product.' },
    });
    await act(async () => {
      fireEvent.click(screen.getByRole('button', { name: /Submit review/i }));
    });
    await waitFor(() => expect(createMock).toHaveBeenCalled());
    const [orderItemId, input] = createMock.mock.calls[0];
    expect(orderItemId).toBe('oi-1');
    expect(input).toMatchObject({
      rating: 5,
      rating_item_as_described: 5,
      rating_shipping_speed: 5,
      rating_communication: 5,
      rating_packaging: 5,
      title: 'Loved it',
      body: 'This is a sufficiently long review of the product.',
    });
    expect(onClose).toHaveBeenCalled();
  });

  it('uploads photos immediately and includes ids on submit', async () => {
    uploadMock.mockResolvedValue({ id: 'att-1', url: 'http://x/x.jpg', thumb_url: 'http://x/t.jpg' });
    createMock.mockResolvedValue({ data: baseReview });
    render(
      wrap(
        <ReviewModal
          orderItem={orderItem}
          storeName="Revive"
          mode="create"
          onClose={() => {}}
        />,
      ),
    );
    const fileInput = screen.getByLabelText(/Add review photos/i) as HTMLInputElement;
    const file = new File(['x'], 'a.jpg', { type: 'image/jpeg' });
    await act(async () => {
      fireEvent.change(fileInput, { target: { files: [file] } });
    });
    await waitFor(() => expect(uploadMock).toHaveBeenCalledWith('oi-1', file));
    expect(screen.getByTestId('review-photo-att-1')).toBeInTheDocument();

    // Fill and submit.
    fireEvent.click(screen.getAllByRole('group')[0].querySelectorAll('button')[4]);
    fireEvent.change(screen.getByLabelText(/Review \*/), {
      target: { value: 'This is a long enough review body to satisfy.' },
    });
    await act(async () => {
      fireEvent.click(screen.getByRole('button', { name: /Submit review/i }));
    });
    await waitFor(() => expect(createMock).toHaveBeenCalled());
    const [, input] = createMock.mock.calls[0];
    expect(input.attachment_ids).toEqual(['att-1']);
  });

  it('remove button drops a photo from staged ids', async () => {
    uploadMock.mockResolvedValue({ id: 'att-2', url: null, thumb_url: null });
    render(
      wrap(
        <ReviewModal
          orderItem={orderItem}
          storeName="Revive"
          mode="create"
          onClose={() => {}}
        />,
      ),
    );
    const fileInput = screen.getByLabelText(/Add review photos/i) as HTMLInputElement;
    const file = new File(['x'], 'a.jpg', { type: 'image/jpeg' });
    await act(async () => {
      fireEvent.change(fileInput, { target: { files: [file] } });
    });
    await waitFor(() =>
      expect(screen.getByTestId('review-photo-att-2')).toBeInTheDocument(),
    );
    fireEvent.click(screen.getByRole('button', { name: /Remove photo att-2/i }));
    expect(screen.queryByTestId('review-photo-att-2')).toBeNull();
  });

  it('edit mode pre-fills from initial review and submits via update', async () => {
    updateMock.mockResolvedValue({ data: { ...baseReview, body: 'updated body of sufficient length' } });
    const onClose = vi.fn();
    render(
      wrap(
        <ReviewModal
          orderItem={orderItem}
          storeName="Revive"
          mode="edit"
          initial={baseReview}
          onClose={onClose}
        />,
      ),
    );
    expect(
      screen.getByText(/Edit your review of Revive/i),
    ).toBeInTheDocument();
    expect((screen.getByLabelText(/Title/i) as HTMLInputElement).value).toBe('Nice');
    expect((screen.getByLabelText(/Review \*/) as HTMLTextAreaElement).value).toContain(
      'perfectly fine',
    );

    fireEvent.change(screen.getByLabelText(/Review \*/), {
      target: { value: 'updated body of sufficient length here' },
    });
    await act(async () => {
      fireEvent.click(screen.getByRole('button', { name: /Save changes/i }));
    });
    await waitFor(() => expect(updateMock).toHaveBeenCalled());
    const [reviewId, input] = updateMock.mock.calls[0];
    expect(reviewId).toBe('rev-1');
    expect(input.body).toBe('updated body of sufficient length here');
    expect(onClose).toHaveBeenCalled();
  });

  it('edit mode allows attaching new photos and submits attachment_ids', async () => {
    uploadMock.mockResolvedValue({
      id: 'att-edit-1',
      url: 'http://x/edit.jpg',
      thumb_url: 'http://x/edit-thumb.jpg',
    });
    updateMock.mockResolvedValue({ data: baseReview });
    render(
      wrap(
        <ReviewModal
          orderItem={orderItem}
          storeName="Revive"
          mode="edit"
          initial={baseReview}
          onClose={() => {}}
        />,
      ),
    );

    // Uploader must be mounted in edit mode.
    const fileInput = screen.getByLabelText(/Add review photos/i) as HTMLInputElement;
    const file = new File(['x'], 'edited.jpg', { type: 'image/jpeg' });
    await act(async () => {
      fireEvent.change(fileInput, { target: { files: [file] } });
    });
    await waitFor(() => expect(uploadMock).toHaveBeenCalledWith('oi-1', file));
    expect(screen.getByTestId('review-photo-att-edit-1')).toBeInTheDocument();

    // Save the edit.
    await act(async () => {
      fireEvent.click(screen.getByRole('button', { name: /Save changes/i }));
    });
    await waitFor(() => expect(updateMock).toHaveBeenCalled());
    const [reviewId, input] = updateMock.mock.calls[0];
    expect(reviewId).toBe('rev-1');
    expect(input.attachment_ids).toEqual(['att-edit-1']);
  });

  it('Esc closes the dialog', () => {
    const onClose = vi.fn();
    render(
      wrap(
        <ReviewModal
          orderItem={orderItem}
          storeName="Revive"
          mode="create"
          onClose={onClose}
        />,
      ),
    );
    fireEvent.keyDown(document, { key: 'Escape' });
    expect(onClose).toHaveBeenCalled();
  });
});
