// src/pages/admin/components/CorrectionRow.test.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
import { CorrectionRow } from './CorrectionRow';
import * as apiClient from '../../../services/apiClient';
import {
  createMockSuggestedCorrection,
  createMockMasterGroceryItem,
  createMockCategory,
} from '../../../tests/utils/mockFactories';

// Cast the mocked module to its mocked type to retain type safety and autocompletion.
// The apiClient is now mocked globally via src/tests/setup/tests-setup-unit.ts.
const mockedApiClient = apiClient as Mocked<typeof apiClient>;

// Mock the logger
vi.mock('../../../services/logger', () => ({
  logger: { info: vi.fn(), error: vi.fn() },
}));

// Mock the ConfirmationModal to test its props and interactions
// The ConfirmationModal is now in a different directory.
// We use ReactDOM.createPortal to avoid rendering a <div> inside a <tbody>, which is invalid HTML.
vi.mock('../../../components/ConfirmationModal', () => ({
  ConfirmationModal: vi.fn(
    ({
      isOpen,
      onConfirm,
      onClose,
    }: {
      isOpen: boolean;
      onConfirm: () => void;
      onClose: () => void;
    }) => {
      if (!isOpen) return null;
      // Use a portal to render the modal at the body level, avoiding invalid nesting.
      return ReactDOM.createPortal(
        <div data-testid="confirmation-modal">
          <button onClick={onConfirm}>Confirm</button>
          <button onClick={onClose}>Cancel</button>
        </div>,
        document.body,
      );
    },
  ),
}));

const mockCorrection = createMockSuggestedCorrection({
  suggested_correction_id: 1,
  flyer_item_id: 101,
  user_id: 'user-1',
  correction_type: 'WRONG_PRICE',
  suggested_value: '250', // $2.50
  status: 'pending',
  flyer_item_name: 'Bananas',
  flyer_item_price_display: '$1.99',
  user_email: 'test@example.com',
});

const mockMasterItems = [
  createMockMasterGroceryItem({
    master_grocery_item_id: 1,
    name: 'Bananas',
    category_id: 1,
    category_name: 'Produce',
  }),
];
const mockCategories = [createMockCategory({ category_id: 1, name: 'Produce' })];

const mockOnProcessed = vi.fn();

const defaultProps = {
  correction: mockCorrection,
  masterItems: mockMasterItems,
  categories: mockCategories,
  onProcessed: mockOnProcessed,
};

// Helper to render the component inside a table structure
const renderInTable = (props = defaultProps) => {
  return render(
    <table>
      <tbody>
        <CorrectionRow {...props} />
      </tbody>
    </table>,
  );
};

describe('CorrectionRow', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockedApiClient.approveCorrection.mockResolvedValue(new Response(null, { status: 204 }));
    mockedApiClient.rejectCorrection.mockResolvedValue(new Response(null, { status: 204 }));
    mockedApiClient.updateSuggestedCorrection.mockResolvedValue(
      new Response(JSON.stringify({ ...mockCorrection, suggested_value: '300' })),
    );
  });

  it('should render correction data correctly', () => {
    renderInTable();
    expect(screen.getByText('Bananas')).toBeInTheDocument();
    expect(screen.getByText('Original Price: $1.99')).toBeInTheDocument();
    expect(screen.getByText('WRONG_PRICE')).toBeInTheDocument();
    expect(screen.getByText('$2.50')).toBeInTheDocument(); // Formatted price
    expect(screen.getByText('test@example.com')).toBeInTheDocument();
  });

  it('should display "Unknown" if user email is missing', () => {
    renderInTable({
      ...defaultProps,
      correction: { ...mockCorrection, user_email: undefined },
    });
    expect(screen.getByText('Unknown')).toBeInTheDocument();
  });

  describe('formatSuggestedValue', () => {
    it('should format INCORRECT_ITEM_LINK with a known item', () => {
      renderInTable({
        ...defaultProps,
        correction: {
          ...mockCorrection,
          correction_type: 'INCORRECT_ITEM_LINK',
          suggested_value: '1',
        },
      });
      expect(screen.getByText('Bananas (ID: 1)')).toBeInTheDocument();
    });

    it('should format INCORRECT_ITEM_LINK with an unknown item ID', () => {
      renderInTable({
        ...defaultProps,
        correction: {
          ...mockCorrection,
          correction_type: 'INCORRECT_ITEM_LINK',
          suggested_value: '999',
        },
      });
      expect(screen.getByText('Unknown Item (ID: 999)')).toBeInTheDocument();
    });

    it('should format ITEM_IS_MISCATEGORIZED with a known category', () => {
      renderInTable({
        ...defaultProps,
        correction: {
          ...mockCorrection,
          correction_type: 'ITEM_IS_MISCATEGORIZED',
          suggested_value: '1',
        },
      });
      expect(screen.getByText('Produce (ID: 1)')).toBeInTheDocument();
    });

    it('should format ITEM_IS_MISCATEGORIZED with an unknown category ID', () => {
      renderInTable({
        ...defaultProps,
        correction: {
          ...mockCorrection,
          correction_type: 'ITEM_IS_MISCATEGORIZED',
          suggested_value: '999',
        },
      });
      expect(screen.getByText('Unknown Category (ID: 999)')).toBeInTheDocument();
    });

    it('should return raw value for WRONG_PRICE if value is not a number', () => {
      renderInTable({
        ...defaultProps,
        correction: {
          ...mockCorrection,
          correction_type: 'WRONG_PRICE',
          suggested_value: 'invalid',
        },
      });
      expect(screen.getByText('invalid')).toBeInTheDocument();
    });

    it('should return the raw value for other correction types', () => {
      renderInTable({
        ...defaultProps,
        correction: {
          ...mockCorrection,
          correction_type: 'OTHER',
          suggested_value: 'Some other value',
        },
      });
      expect(screen.getByText('Some other value')).toBeInTheDocument();
    });
  });

  it('should open the confirmation modal on approve click', async () => {
    renderInTable();
    fireEvent.click(screen.getByTitle('Approve'));
    await waitFor(() => {
      expect(screen.getByTestId('confirmation-modal')).toBeInTheDocument();
    });
  });

  it('should call approveCorrection when approval is confirmed', async () => {
    renderInTable();
    fireEvent.click(screen.getByTitle('Approve'));
    await waitFor(() => {
      fireEvent.click(screen.getByRole('button', { name: 'Confirm' }));
    });
    await waitFor(() => {
      expect(mockedApiClient.approveCorrection).toHaveBeenCalledWith(
        mockCorrection.suggested_correction_id,
      );
      expect(mockOnProcessed).toHaveBeenCalledWith(mockCorrection.suggested_correction_id);
    });
  });

  it('should call rejectCorrection when rejection is confirmed', async () => {
    renderInTable();
    fireEvent.click(screen.getByTitle('Reject'));
    await waitFor(() => {
      expect(screen.getByTestId('confirmation-modal')).toBeInTheDocument();
      fireEvent.click(screen.getByRole('button', { name: 'Confirm' }));
    });
    await waitFor(() => {
      expect(mockedApiClient.rejectCorrection).toHaveBeenCalledWith(
        mockCorrection.suggested_correction_id,
      );
      expect(mockOnProcessed).toHaveBeenCalledWith(mockCorrection.suggested_correction_id);
    });
  });

  it('should display an error message if an action fails', async () => {
    mockedApiClient.approveCorrection.mockRejectedValue(new Error('API Error'));
    renderInTable();
    fireEvent.click(screen.getByTitle('Approve'));
    await waitFor(() => {
      fireEvent.click(screen.getByRole('button', { name: 'Confirm' }));
    });
    await waitFor(() => {
      expect(screen.getByText('API Error')).toBeInTheDocument();
    });
  });

  it('should handle non-Error objects thrown during confirmation', async () => {
    // Mock rejection with a string (not an Error object) to test the fallback error message
    mockedApiClient.approveCorrection.mockRejectedValue('Network string error');

    renderInTable();
    fireEvent.click(screen.getByTitle('Approve'));

    // Wait for modal
    await waitFor(() => expect(screen.getByTestId('confirmation-modal')).toBeInTheDocument());

    fireEvent.click(screen.getByRole('button', { name: 'Confirm' }));

    await waitFor(() => {
      // Expect the fallback message defined in the component
      expect(
        screen.getByText('An unknown error occurred while trying to approve the correction.'),
      ).toBeInTheDocument();
    });
  });

  it('should enter and exit editing mode', async () => {
    renderInTable();
    // Enter editing mode
    fireEvent.click(screen.getByTitle('Edit'));
    await waitFor(() => {
      expect(screen.getByRole('spinbutton')).toBeInTheDocument(); // For input type=number
      expect(screen.getByTitle('Save')).toBeInTheDocument();
      expect(screen.getByTitle('Cancel')).toBeInTheDocument();
    });

    // Exit editing mode
    fireEvent.click(screen.getByTitle('Cancel'));
    await waitFor(() => {
      expect(screen.queryByRole('spinbutton')).not.toBeInTheDocument();
      expect(screen.getByTitle('Approve')).toBeInTheDocument();
    });
  });

  it('should save an edited value', async () => {
    renderInTable();
    fireEvent.click(screen.getByTitle('Edit'));

    const input = await screen.findByRole('spinbutton');
    fireEvent.change(input, { target: { value: '300' } });
    fireEvent.click(screen.getByTitle('Save'));

    await waitFor(() => {
      expect(mockedApiClient.updateSuggestedCorrection).toHaveBeenCalledWith(
        mockCorrection.suggested_correction_id,
        '300',
      );
      // The component should now display the updated value from the mock response
      expect(screen.getByText('$3.00')).toBeInTheDocument();
    });

    // Check that it exited editing mode
    expect(screen.queryByRole('spinbutton')).not.toBeInTheDocument();
  });

  it('should display an error if saving an edit fails', async () => {
    mockedApiClient.updateSuggestedCorrection.mockRejectedValue(new Error('Update failed'));
    renderInTable();
    fireEvent.click(screen.getByTitle('Edit'));

    const input = await screen.findByRole('spinbutton');
    fireEvent.change(input, { target: { value: '300' } });
    fireEvent.click(screen.getByTitle('Save'));

    await waitFor(() => {
      expect(screen.getByText('Update failed')).toBeInTheDocument();
    });
    // It should remain in editing mode
    expect(screen.getByRole('spinbutton')).toBeInTheDocument();
  });

  it('should display a generic error if saving an edit fails with a non-Error object', async () => {
    mockedApiClient.updateSuggestedCorrection.mockRejectedValue('String error');
    renderInTable();
    fireEvent.click(screen.getByTitle('Edit'));

    const input = await screen.findByRole('spinbutton');
    fireEvent.change(input, { target: { value: '300' } });
    fireEvent.click(screen.getByTitle('Save'));

    await waitFor(() => {
      // Expect the fallback message defined in the component
      expect(screen.getByText('Failed to save changes.')).toBeInTheDocument();
    });
    expect(screen.getByRole('spinbutton')).toBeInTheDocument();
  });

  it('should display an error if saving an edit returns invalid JSON', async () => {
    // Mock a response that is OK but has invalid JSON, which will cause .json() to throw
    mockedApiClient.updateSuggestedCorrection.mockResolvedValue(
      new Response('not json', { status: 200 }),
    );
    renderInTable();
    fireEvent.click(screen.getByTitle('Edit'));

    const input = await screen.findByRole('spinbutton');
    fireEvent.change(input, { target: { value: '300' } });
    fireEvent.click(screen.getByTitle('Save'));

    await waitFor(() => {
      // The error message from a JSON.parse error is implementation-dependent.
      // We can check that the catch block's fallback message is NOT used, and that AN error is shown.
      // Vitest/JSDOM might show something like "Unexpected token 'o', \"not json\" is not valid JSON"
      expect(screen.queryByText('Failed to save changes.')).not.toBeInTheDocument();
      expect(screen.getByText(/not valid JSON/i)).toBeInTheDocument();
    });
    // It should remain in editing mode
    expect(screen.getByRole('spinbutton')).toBeInTheDocument();
  });

  it('should save an edited value for INCORRECT_ITEM_LINK', async () => {
    // Add a temporary item to the master list for this test
    const localMasterItems = [
      ...mockMasterItems,
      createMockMasterGroceryItem({
        master_grocery_item_id: 2,
        name: 'Milk',
        category_id: 2,
        category_name: 'Dairy',
      }),
    ];
    mockedApiClient.updateSuggestedCorrection.mockResolvedValue(
      new Response(
        JSON.stringify({
          ...mockCorrection,
          correction_type: 'INCORRECT_ITEM_LINK',
          suggested_value: '2',
        }),
      ),
    );

    renderInTable({
      ...defaultProps,
      masterItems: localMasterItems, // Use local version
      correction: {
        ...mockCorrection,
        correction_type: 'INCORRECT_ITEM_LINK',
        suggested_value: '1',
      },
    });
    fireEvent.click(screen.getByTitle('Edit'));

    const select = await screen.findByRole('combobox');
    fireEvent.change(select, { target: { value: '2' } }); // Change to 'Milk'
    fireEvent.click(screen.getByTitle('Save'));

    await waitFor(() => {
      expect(mockedApiClient.updateSuggestedCorrection).toHaveBeenCalledWith(
        mockCorrection.suggested_correction_id,
        '2',
      );
      // The component should now display the updated value from the mock response
      expect(screen.getByText('Milk (ID: 2)')).toBeInTheDocument();
    });
  });

  it('should save an edited value for ITEM_IS_MISCATEGORIZED', async () => {
    const localCategories = [
      ...mockCategories,
      createMockCategory({ category_id: 2, name: 'Dairy' }),
    ];
    mockedApiClient.updateSuggestedCorrection.mockResolvedValue(
      new Response(
        JSON.stringify({
          ...mockCorrection,
          correction_type: 'ITEM_IS_MISCATEGORIZED',
          suggested_value: '2',
        }),
      ),
    );

    renderInTable({
      ...defaultProps,
      categories: localCategories, // Use local version
      correction: {
        ...mockCorrection,
        correction_type: 'ITEM_IS_MISCATEGORIZED',
        suggested_value: '1',
      },
    });
    fireEvent.click(screen.getByTitle('Edit'));

    const select = await screen.findByRole('combobox');
    fireEvent.change(select, { target: { value: '2' } }); // Change to 'Dairy'
    fireEvent.click(screen.getByTitle('Save'));

    await waitFor(() => {
      expect(mockedApiClient.updateSuggestedCorrection).toHaveBeenCalledWith(
        mockCorrection.suggested_correction_id,
        '2',
      );
      expect(screen.getByText('Dairy (ID: 2)')).toBeInTheDocument();
    });
  });

  it('should save an edited value for OTHER correction type', async () => {
    mockedApiClient.updateSuggestedCorrection.mockResolvedValue(
      new Response(
        JSON.stringify({
          ...mockCorrection,
          correction_type: 'OTHER',
          suggested_value: 'A new other value',
        }),
      ),
    );

    renderInTable({
      ...defaultProps,
      correction: {
        ...mockCorrection,
        correction_type: 'OTHER',
        suggested_value: 'Some other value',
      },
    });
    fireEvent.click(screen.getByTitle('Edit'));

    const input = await screen.findByRole('textbox');
    fireEvent.change(input, { target: { value: 'A new other value' } });
    fireEvent.click(screen.getByTitle('Save'));

    await waitFor(() => {
      expect(mockedApiClient.updateSuggestedCorrection).toHaveBeenCalledWith(
        mockCorrection.suggested_correction_id,
        'A new other value',
      );
      expect(screen.getByText('A new other value')).toBeInTheDocument();
    });
  });

  describe('renderEditableField', () => {
    it('should render a select for INCORRECT_ITEM_LINK', async () => {
      renderInTable({
        ...defaultProps,
        correction: {
          ...mockCorrection,
          correction_type: 'INCORRECT_ITEM_LINK',
          suggested_value: '1',
        },
      });

      fireEvent.click(screen.getByTitle('Edit'));

      const select = await screen.findByRole('combobox');
      expect(select).toBeInTheDocument();
      expect(select.querySelectorAll('option')).toHaveLength(mockMasterItems.length);

      // FIX: Use getByRole to specifically target the option,
      // which distinguishes it from the 'Bananas' text in the flyer item name cell.
      expect(screen.getByRole('option', { name: 'Bananas' })).toBeInTheDocument();
    });

    it('should render a select for ITEM_IS_MISCATEGORIZED', async () => {
      renderInTable({
        ...defaultProps,
        correction: {
          ...mockCorrection,
          correction_type: 'ITEM_IS_MISCATEGORIZED',
          suggested_value: '1',
        },
      });

      fireEvent.click(screen.getByTitle('Edit'));

      const select = await screen.findByRole('combobox');
      expect(select).toBeInTheDocument();
      expect(select.querySelectorAll('option')).toHaveLength(mockCategories.length);

      // FIX: Use getByRole for consistency and robustness
      expect(screen.getByRole('option', { name: 'Produce' })).toBeInTheDocument();
    });

    it('should render a text input for unknown correction types', async () => {
      renderInTable({
        ...defaultProps,
        correction: {
          ...mockCorrection,
          correction_type: 'OTHER_TYPE' as any,
          suggested_value: 'some text',
        },
      });

      fireEvent.click(screen.getByTitle('Edit'));

      const input = await screen.findByRole('textbox'); // type="text" has role textbox
      expect(input).toBeInTheDocument();
      expect(input).toHaveValue('some text');
    });
  });
});
