// src/services/aiApiClient.test.ts
import { describe, it, expect, vi, beforeAll, afterAll, afterEach } from 'vitest';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';

// Ensure the module under test is NOT mocked.
vi.unmock('./aiApiClient');

import * as aiApiClient from './aiApiClient';

// 1. Mock logger to keep output clean
vi.mock('./logger.client', () => ({
  logger: {
    debug: vi.fn(),
    info: vi.fn(),
    error: vi.fn(),
  },
}));

// 2. Mock ./apiClient to simply pass calls through to the global fetch.
vi.mock('./apiClient', async (importOriginal) => {
  const actual = await importOriginal<typeof import('./apiClient')>();
  return {
  apiFetch: (url: string, options: RequestInit = {}, apiOptions: import('./apiClient').ApiOptions = {}) => {
    // The base URL must match what MSW is expecting.
    const fullUrl = url.startsWith('/')
      ? `http://localhost/api${url}`
      : url;
    // FIX: Correctly handle the tokenOverride by merging it into the request headers.
    if (apiOptions.tokenOverride) {
      options.headers = { ...options.headers, Authorization: `Bearer ${apiOptions.tokenOverride}` };
    }

    // FIX: Manually construct a Request object. This ensures that when `options.body`
    // is FormData, the contained File objects are correctly processed by MSW's parsers,
    // preserving their original filenames instead of defaulting to "blob".
    return fetch(new Request(fullUrl, options));
  },
  // Add a mock for ApiOptions to satisfy the compiler
  ApiOptions: vi.fn()
  };
});

// 3. Setup MSW to capture requests
const requestSpy = vi.fn();

const server = setupServer(
  // Handler for all POST requests to the AI endpoints
  http.post('http://localhost/api/ai/:endpoint', async ({ request, params }) => {
    let body: Record<string, unknown> | FormData = {};
    const contentType = request.headers.get('Content-Type');

    if (contentType?.includes('application/json')) {
      const parsedBody = await request.json();
      // Ensure the parsed body is an object before assigning, as request.json() can return primitives.
      if (typeof parsedBody === 'object' && parsedBody !== null && !Array.isArray(parsedBody)) {
        body = parsedBody as Record<string, unknown>;
      }
    } else if (contentType?.includes('multipart/form-data')) {
      body = await request.formData();
      // When MSW processes FormData in JSDOM, the file.name property is lost.
      // To work around this, we iterate through the FormData. The `value` object
      // is a File-like object that still retains its original `name`. We attach this
      // to a custom property that our test assertions can reliably use.
      (body as FormData).forEach((value, _key) => {
        if (value instanceof File) {
          (value as File & { originalName: string }).originalName = value.name;
        }
      });
    }

    requestSpy({
      endpoint: params.endpoint,
      method: request.method,
      body,
      headers: request.headers,
    });

    return HttpResponse.json({ success: true });
  }),

  // Handler for GET requests, specifically for job status
  http.get('http://localhost/api/ai/jobs/:jobId/status', ({ params }) => {
    requestSpy({
      endpoint: 'jobs',
      method: 'GET',
      jobId: params.jobId,
    });
    return HttpResponse.json({ state: 'completed' });
  })
);

describe('AI API Client (Network Mocking with MSW)', () => {
  beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
  
  afterEach(() => {
    server.resetHandlers();
    requestSpy.mockClear();
    vi.clearAllMocks();
  });
  
  afterAll(() => server.close());

  describe('uploadAndProcessFlyer', () => {
    it('should construct FormData with file and checksum and send a POST request', async () => {
      const mockFile = new File(['dummy-flyer-content'], 'flyer.pdf', { type: 'application/pdf' });
      const checksum = 'checksum-abc-123';

      await aiApiClient.uploadAndProcessFlyer(mockFile, checksum);

      expect(requestSpy).toHaveBeenCalledTimes(1);
      const req = requestSpy.mock.calls[0][0];

      expect(req.endpoint).toBe('upload-and-process');
      expect(req.method).toBe('POST');
      // FIX: Use a duck-typing check for FormData to avoid environment-specific instance issues.
      expect(typeof (req.body as FormData).get).toBe('function');
      
      const flyerFile = (req.body as FormData).get('flyerFile') as File & { originalName: string };
      const checksumValue = (req.body as FormData).get('checksum');
      
      expect(flyerFile.originalName).toBe('flyer.pdf');
      expect(checksumValue).toBe(checksum);
    });
  });

  describe('getJobStatus', () => {
    it('should send a GET request to the correct job status URL', async () => {
      const jobId = 'job-id-456';
      await aiApiClient.getJobStatus(jobId);

      expect(requestSpy).toHaveBeenCalledTimes(1);
      const req = requestSpy.mock.calls[0][0];

      expect(req.endpoint).toBe('jobs');
      expect(req.method).toBe('GET');
      expect(req.jobId).toBe(jobId);
    });

  });

  describe('isImageAFlyer', () => {
    it('should construct FormData and send a POST request', async () => {
      const mockFile = new File(['dummy'], 'flyer.jpg', { type: 'image/jpeg' });
      await aiApiClient.isImageAFlyer(mockFile, 'test-token');

      expect(requestSpy).toHaveBeenCalledTimes(1);
      const req = requestSpy.mock.calls[0][0];
      
      expect(req.endpoint).toBe('check-flyer');
      expect(req.method).toBe('POST');
      expect(typeof (req.body as FormData).get).toBe('function');
      const imageFile = (req.body as FormData).get('image') as File & { originalName: string };
      expect(imageFile.originalName).toBe('flyer.jpg');
      expect(req.headers.get('Authorization')).toBe('Bearer test-token');
    });
  });

  describe('extractAddressFromImage', () => {
    it('should construct FormData and send a POST request', async () => {
      const mockFile = new File(['dummy'], 'flyer.jpg', { type: 'image/jpeg' });
      await aiApiClient.extractAddressFromImage(mockFile, 'test-token');

      expect(requestSpy).toHaveBeenCalledTimes(1);
      const req = requestSpy.mock.calls[0][0];

      expect(req.endpoint).toBe('extract-address');
      expect(typeof (req.body as FormData).get).toBe('function');
      const imageFile = (req.body as FormData).get('image') as File & { originalName: string };
      expect(imageFile.originalName).toBe('flyer.jpg');
      expect(req.headers.get('Authorization')).toBe('Bearer test-token');
    });
  });

  describe('extractLogoFromImage', () => {
    it('should construct FormData and send a POST request', async () => {
      const mockFile = new File(['logo'], 'logo.jpg', { type: 'image/jpeg' });
      await aiApiClient.extractLogoFromImage([mockFile], 'test-token');

      expect(requestSpy).toHaveBeenCalledTimes(1);
      const req = requestSpy.mock.calls[0][0];

      expect(req.endpoint).toBe('extract-logo');
      expect(typeof (req.body as FormData).get).toBe('function');
      const imageFile = (req.body as FormData).get('images') as File & { originalName: string };
      expect(imageFile.originalName).toBe('logo.jpg');
      expect(req.headers.get('Authorization')).toBe('Bearer test-token');
    });
  });

  describe('getQuickInsights', () => {
    it('should send items as JSON in the body', async () => {
      const items = [{ item: 'apple' }];
      await aiApiClient.getQuickInsights(items, undefined, 'test-token');

      expect(requestSpy).toHaveBeenCalledTimes(1);
      const req = requestSpy.mock.calls[0][0];

      expect(req.endpoint).toBe('quick-insights');
      expect(req.body).toEqual({ items });
      expect(req.headers.get('Authorization')).toBe('Bearer test-token');
    });
  });

  describe('getDeepDiveAnalysis', () => {
    it('should send items as JSON in the body', async () => {
      const items = [{ item: 'apple' }];
      await aiApiClient.getDeepDiveAnalysis(items, undefined, 'test-token');

      expect(requestSpy).toHaveBeenCalledTimes(1);
      const req = requestSpy.mock.calls[0][0];

      expect(req.endpoint).toBe('deep-dive');
      expect(req.body).toEqual({ items });
      expect(req.headers.get('Authorization')).toBe('Bearer test-token');
    });
  });

  describe('searchWeb', () => {
    it('should send items as JSON in the body', async () => {
      const items = [{ item: 'search me' }];
      await aiApiClient.searchWeb(items, undefined, 'test-token');

      expect(requestSpy).toHaveBeenCalledTimes(1);
      const req = requestSpy.mock.calls[0][0];

      expect(req.endpoint).toBe('search-web');
      expect(req.body).toEqual({ items });
      expect(req.headers.get('Authorization')).toBe('Bearer test-token');
    });
  });

  describe('generateImageFromText', () => {
    it('should send prompt as JSON in the body', async () => {
      const prompt = 'A tasty burger';
      await aiApiClient.generateImageFromText(prompt, undefined, 'test-token');

      expect(requestSpy).toHaveBeenCalledTimes(1);
      const req = requestSpy.mock.calls[0][0];

      expect(req.endpoint).toBe('generate-image');
      expect(req.body).toEqual({ prompt });
      expect(req.headers.get('Authorization')).toBe('Bearer test-token');
    });
  });

  describe('generateSpeechFromText', () => {
    it('should send text as JSON in the body', async () => {
      const text = 'Hello world';
      await aiApiClient.generateSpeechFromText(text, undefined, 'test-token');

      expect(requestSpy).toHaveBeenCalledTimes(1);
      const req = requestSpy.mock.calls[0][0];

      expect(req.endpoint).toBe('generate-speech');
      expect(req.body).toEqual({ text });
      expect(req.headers.get('Authorization')).toBe('Bearer test-token');
    });
  });

  describe('planTripWithMaps', () => {
    it('should send items, store, and location as JSON in the body', async () => {
      // Create a full FlyerItem object, as the function signature requires it, not a partial.
      const items: import('../types').FlyerItem[] = [{
        flyer_item_id: 1,
        flyer_id: 1,
        item: 'bread',
        price_display: '$1.99',
        price_in_cents: 199,
        quantity: '1 loaf',
        category_name: 'Bakery',
        view_count: 0,
        click_count: 0,
        updated_at: new Date().toISOString(),
        created_at: new Date().toISOString(),
      }];
      const store: import('../types').Store = { store_id: 1, name: 'Test Store', created_at: new Date().toISOString() };
      const userLocation: GeolocationCoordinates = { latitude: 45, longitude: -75, accuracy: 0, altitude: null, altitudeAccuracy: null, heading: null, speed: null, toJSON: () => ({}) };

      await aiApiClient.planTripWithMaps(items, store, userLocation);

      expect(requestSpy).toHaveBeenCalledTimes(1);
      const req = requestSpy.mock.calls[0][0];

      expect(req.endpoint).toBe('plan-trip');
      expect(req.body).toEqual({ items, store, userLocation });
    });
  });

  describe('rescanImageArea', () => {
    it('should construct FormData with image, cropArea, and extractionType', async () => {
      const mockFile = new File(['dummy-content'], 'flyer-page.jpg', { type: 'image/jpeg' });
      const cropArea = { x: 10, y: 20, width: 100, height: 50 };
      const extractionType = 'item_details' as const;

      await aiApiClient.rescanImageArea(mockFile, cropArea, extractionType);

      expect(requestSpy).toHaveBeenCalledTimes(1);
      const req = requestSpy.mock.calls[0][0];

      expect(req.endpoint).toBe('rescan-area');
      expect(typeof (req.body as FormData).get).toBe('function');

      const imageFile = (req.body as FormData).get('image') as File & { originalName: string };
      const cropAreaValue = (req.body as FormData).get('cropArea');
      const extractionTypeValue = (req.body as FormData).get('extractionType');

      expect(imageFile.originalName).toBe('flyer-page.jpg');
      expect(cropAreaValue).toBe(JSON.stringify(cropArea));
      expect(extractionTypeValue).toBe(extractionType);
    });
  });

  describe('startVoiceSession', () => {
    it('should throw an error as it is not implemented', () => {
      const mockCallbacks = {
        onmessage: vi.fn(),
        onopen: vi.fn(),
        onclose: vi.fn(),
      };
      expect(() => aiApiClient.startVoiceSession(mockCallbacks)).toThrow(
        'Voice session feature is not fully implemented and requires a backend WebSocket proxy.'
      );
    });
  });
});