// src/components/FlyerCorrectionTool.test.tsx
import React from 'react';
import { screen, fireEvent, waitFor, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
import { FlyerCorrectionTool } from './FlyerCorrectionTool';
import * as aiApiClient from '../services/aiApiClient';
import { notifyError, notifySuccess } from '../services/notificationService';
import { renderWithProviders } from '../tests/utils/renderWithProviders';

// Unmock the component to test the real implementation
vi.unmock('./FlyerCorrectionTool');

// The aiApiClient, notificationService, and logger are mocked globally.
// We can get a typed reference to the aiApiClient for individual test overrides.
const mockedAiApiClient = vi.mocked(aiApiClient);
const mockedNotifySuccess = notifySuccess as Mocked<typeof notifySuccess>;
const mockedNotifyError = notifyError as Mocked<typeof notifyError>;

const defaultProps = {
  isOpen: true,
  onClose: vi.fn(),
  imageUrl: 'http://example.com/flyer.jpg',
  onDataExtracted: vi.fn(),
};

describe('FlyerCorrectionTool', () => {
  beforeEach(() => {
    vi.clearAllMocks();

    // Mock global fetch for fetching the image blob inside the component
    global.fetch = vi.fn(() =>
      Promise.resolve(new Response(new Blob(['dummy-image-content'], { type: 'image/jpeg' }))),
    ) as Mocked<typeof fetch>;

    // Mock canvas methods for jsdom environment
    window.HTMLCanvasElement.prototype.getContext = vi.fn((contextId: string) => {
      if (contextId === '2d') {
        return {
          clearRect: vi.fn(),
          strokeRect: vi.fn(),
          setLineDash: vi.fn(),
          strokeStyle: '',
          lineWidth: 0,
        } as unknown as CanvasRenderingContext2D;
      }
      return null;
    }) as typeof window.HTMLCanvasElement.prototype.getContext;
  });

  it('should not render when isOpen is false', () => {
    const { container } = renderWithProviders(<FlyerCorrectionTool {...defaultProps} isOpen={false} />);
    expect(container.firstChild).toBeNull();
  });

  it('should render correctly when isOpen is true', () => {
    renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);
    expect(screen.getByRole('heading', { name: /flyer correction tool/i })).toBeInTheDocument();
    expect(screen.getByAltText('Flyer for correction')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /extract store name/i })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /extract sale dates/i })).toBeInTheDocument();
  });

  it('should call onClose when the close button is clicked', () => {
    renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);
    // Use the specific aria-label defined in the component to find the close button
    const closeButton = screen.getByLabelText(/close correction tool/i);
    fireEvent.click(closeButton);
    expect(defaultProps.onClose).toHaveBeenCalledTimes(1);
  });

  it('should have disabled extraction buttons initially', () => {
    renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);
    expect(screen.getByRole('button', { name: /extract store name/i })).toBeDisabled();
    expect(screen.getByRole('button', { name: /extract sale dates/i })).toBeDisabled();
  });

  it('should enable extraction buttons after a selection is made', () => {
    renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);
    const canvas = screen.getByRole('dialog').querySelector('canvas')!;

    // Simulate drawing a rectangle
    fireEvent.mouseDown(canvas, { clientX: 10, clientY: 10 });
    fireEvent.mouseMove(canvas, { clientX: 100, clientY: 50 });
    fireEvent.mouseUp(canvas);

    expect(screen.getByRole('button', { name: /extract store name/i })).toBeEnabled();
    expect(screen.getByRole('button', { name: /extract sale dates/i })).toBeEnabled();
  });

  it('should stop drawing when the mouse leaves the canvas', () => {
    renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);
    const canvas = screen.getByRole('dialog').querySelector('canvas')!;

    fireEvent.mouseDown(canvas, { clientX: 10, clientY: 10 });
    fireEvent.mouseMove(canvas, { clientX: 100, clientY: 50 });
    fireEvent.mouseLeave(canvas); // Simulate mouse leaving
    expect(screen.getByRole('button', { name: /extract store name/i })).toBeEnabled();
  });

  it('should call rescanImageArea with correct parameters and show success', async () => {
    console.log('\n--- [TEST LOG] ---: Starting test: "should call rescanImageArea..."');

    // 1. Create a controllable promise for the mock.
    console.log('--- [TEST LOG] ---: 1. Setting up controllable promise for rescanImageArea.');
    let resolveRescanPromise: (value: Response | PromiseLike<Response>) => void;
    const rescanPromise = new Promise<Response>((resolve) => {
      resolveRescanPromise = resolve;
    });
    mockedAiApiClient.rescanImageArea.mockReturnValue(rescanPromise);

    renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);

    // Wait for the image fetch to complete to ensure 'imageFile' state is populated
    console.log('--- [TEST LOG] ---: Awaiting image fetch inside component...');
    await waitFor(() => expect(global.fetch).toHaveBeenCalledWith(defaultProps.imageUrl));
    console.log('--- [TEST LOG] ---: Image fetch complete.');

    const canvas = screen.getByRole('dialog').querySelector('canvas')!;
    const image = screen.getByAltText('Flyer for correction');

    // Mock image dimensions for coordinate scaling
    console.log('--- [TEST LOG] ---: Mocking image dimensions.');
    Object.defineProperty(image, 'naturalWidth', { value: 1000, configurable: true });
    Object.defineProperty(image, 'naturalHeight', { value: 800, configurable: true });
    Object.defineProperty(image, 'clientWidth', { value: 500, configurable: true });
    Object.defineProperty(image, 'clientHeight', { value: 400, configurable: true });

    // Simulate drawing a rectangle
    console.log('--- [TEST LOG] ---: Simulating user drawing a rectangle...');
    fireEvent.mouseDown(canvas, { clientX: 10, clientY: 10 });
    fireEvent.mouseMove(canvas, { clientX: 60, clientY: 30 });
    fireEvent.mouseUp(canvas);
    console.log('--- [TEST LOG] ---: Rectangle drawn.');

    // 2. Click the extract button, which will trigger the pending promise.
    console.log('--- [TEST LOG] ---: 2. Clicking "Extract Store Name" button.');
    fireEvent.click(screen.getByRole('button', { name: /extract store name/i }));

    // 3. Assert the loading state.
    try {
      console.log('--- [TEST LOG] ---: 3. Awaiting "Processing..." loading state.');
      expect(await screen.findByText('Processing...')).toBeInTheDocument();
      console.log('--- [TEST LOG] ---: 3a. SUCCESS: Found "Processing..." text.');
    } catch (error) {
      console.error('--- [TEST LOG] ---: 3a. ERROR: Did not find "Processing..." text.');
      screen.debug();
      throw error;
    }

    // 4. Check that the API was called with correctly scaled coordinates.
    console.log('--- [TEST LOG] ---: 4. Awaiting API call verification...');
    await waitFor(() => {
      console.log('--- [TEST LOG] ---: 4a. waitFor check: Checking rescanImageArea call...');
      expect(mockedAiApiClient.rescanImageArea).toHaveBeenCalledTimes(1);
      expect(mockedAiApiClient.rescanImageArea).toHaveBeenCalledWith(
        expect.any(File),
        // 10*2=20, 10*2=20, (60-10)*2=100, (30-10)*2=40
        { x: 20, y: 20, width: 100, height: 40 },
        'store_name',
      );
    });
    console.log('--- [TEST LOG] ---: 4b. SUCCESS: API call verified.');

    // 5. Resolve the promise.
    console.log('--- [TEST LOG] ---: 5. Manually resolving the API promise inside act()...');
    await act(async () => {
      console.log('--- [TEST LOG] ---: 5a. Calling resolveRescanPromise...');
      resolveRescanPromise(new Response(JSON.stringify({ text: 'Super Store' })));
    });
    console.log('--- [TEST LOG] ---: 5b. Promise resolved and act() block finished.');

    // 6. Assert the final state after the promise has resolved.
    console.log('--- [TEST LOG] ---: 6. Awaiting final state assertions...');
    await waitFor(() => {
      console.log(
        '--- [TEST LOG] ---: 6a. waitFor check: Verifying notifications and callbacks...',
      );
      expect(mockedNotifySuccess).toHaveBeenCalledWith('Extracted: Super Store');
      expect(defaultProps.onDataExtracted).toHaveBeenCalledWith('store_name', 'Super Store');
      expect(defaultProps.onClose).toHaveBeenCalledTimes(1);
    });
    console.log('--- [TEST LOG] ---: 6b. SUCCESS: Final state verified.');
  });

  it('should show an error notification if image fetching fails', async () => {
    // Mock fetch to reject
    global.fetch = vi.fn(() => Promise.reject(new Error('Network error'))) as Mocked<typeof fetch>;

    renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);

    await waitFor(() => {
      expect(mockedNotifyError).toHaveBeenCalledWith('Could not load the image for correction.');
    });
  });

  it('should show an error if rescan is attempted before image is loaded', async () => {
    console.log(
      'TEST: Starting "should show an error if rescan is attempted before image is loaded"',
    );

    // Override fetch to be pending forever so 'imageFile' remains null
    // This allows us to test the guard clause inside handleRescan while the button is enabled
    global.fetch = vi.fn(() => {
      console.log('TEST: fetch called, returning pending promise to simulate loading');
      return new Promise(() => {});
    }) as Mocked<typeof fetch>;

    renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);

    const canvas = screen.getByRole('dialog').querySelector('canvas')!;

    // Draw a selection to enable the button (bypassing the disabled={!selectionRect} check)
    console.log('TEST: Drawing selection to enable button');
    fireEvent.mouseDown(canvas, { clientX: 10, clientY: 10 });
    fireEvent.mouseMove(canvas, { clientX: 100, clientY: 50 });
    fireEvent.mouseUp(canvas);

    const extractButton = screen.getByRole('button', { name: /extract store name/i });
    expect(extractButton).toBeEnabled();
    console.log('TEST: Button is enabled, clicking now...');

    // Attempt rescan.
    // - selectionRect is present (button enabled)
    // - imageFile is null (fetch pending)
    // -> Should trigger guard and notifyError
    fireEvent.click(extractButton);

    console.log('TEST: Checking for error notification');
    expect(mockedNotifyError).toHaveBeenCalledWith('Please select an area on the image first.');
  });

  it('should handle non-standard API errors during rescan', async () => {
    console.log('TEST: Starting "should handle non-standard API errors during rescan"');
    mockedAiApiClient.rescanImageArea.mockRejectedValue('A plain string error');
    renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);

    // Wait for image fetch to ensure imageFile is set before we interact
    await waitFor(() => expect(global.fetch).toHaveBeenCalled());
    // Allow the promise chain in useEffect to complete
    await act(async () => {
      await new Promise((resolve) => setTimeout(resolve, 0));
    });

    const canvas = screen.getByRole('dialog').querySelector('canvas')!;
    fireEvent.mouseDown(canvas, { clientX: 10, clientY: 10 });
    fireEvent.mouseMove(canvas, { clientX: 100, clientY: 50 });
    fireEvent.mouseUp(canvas);

    console.log('TEST: Clicking button to trigger API error');
    fireEvent.click(screen.getByRole('button', { name: /extract store name/i }));
    await waitFor(() => {
      expect(mockedNotifyError).toHaveBeenCalledWith('An unknown error occurred.');
    });
  });

  it('should handle API failure response (ok: false) correctly', async () => {
    console.log('TEST: Starting "should handle API failure response (ok: false) correctly"');
    mockedAiApiClient.rescanImageArea.mockResolvedValue({
      ok: false,
      json: async () => ({ message: 'Custom API Error' }),
    } as Response);

    renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);

    // Wait for image fetch
    await waitFor(() => expect(global.fetch).toHaveBeenCalled());

    // Draw selection
    const canvas = screen.getByRole('dialog').querySelector('canvas')!;
    fireEvent.mouseDown(canvas, { clientX: 10, clientY: 10 });
    fireEvent.mouseMove(canvas, { clientX: 50, clientY: 50 });
    fireEvent.mouseUp(canvas);

    // Click extract
    fireEvent.click(screen.getByRole('button', { name: /extract store name/i }));

    await waitFor(() => {
      expect(mockedNotifyError).toHaveBeenCalledWith('Custom API Error');
    });
  });

  it('should redraw the canvas when the image loads', () => {
    console.log('TEST: Starting "should redraw the canvas when the image loads"');
    const clearRectSpy = vi.fn();
    // Override the getContext mock for this test to capture the spy
    window.HTMLCanvasElement.prototype.getContext = vi.fn(() => ({
      clearRect: clearRectSpy,
      strokeRect: vi.fn(),
      setLineDash: vi.fn(),
      strokeStyle: '',
      lineWidth: 0,
    })) as any;

    renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);
    const image = screen.getByAltText('Flyer for correction');

    // The draw function is called on mount via useEffect, so we clear that call.
    clearRectSpy.mockClear();

    // Simulate image load event which triggers onLoad={draw}
    fireEvent.load(image);

    expect(clearRectSpy).toHaveBeenCalled();
  });
});
