// src/tests/setup/tests-setup-unit.ts
import { vi, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import type { Request, Response, NextFunction } from 'express';
import '@testing-library/jest-dom/vitest';

// Mock the GeolocationPositionError global that exists in browsers but not in JSDOM.
// This is necessary for tests that simulate geolocation failures.
if (typeof global.GeolocationPositionError === 'undefined') {
  // Define a simple class that mimics the structure of the real GeolocationPositionError.
  global.GeolocationPositionError = class GeolocationPositionError extends Error {
    readonly code: number;
    // Add static properties to match the browser's built-in type definition.
    static readonly PERMISSION_DENIED = 1;
    static readonly POSITION_UNAVAILABLE = 2;
    static readonly TIMEOUT = 3;

    readonly PERMISSION_DENIED = 1;
    readonly POSITION_UNAVAILABLE = 2;
    readonly TIMEOUT = 3;

    // Make constructor arguments optional to match the built-in type definition.
    constructor(message?: string, code?: number) {
      super(message);
      // Provide a default value (e.g., 0) if code is undefined to satisfy the type.
      this.code = code ?? 0;
    }
  };
}

// Mock window.matchMedia, which is not implemented in JSDOM.
// This is necessary for components that check for the user's preferred color scheme.
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: vi.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: vi.fn(), // deprecated
    removeListener: vi.fn(), // deprecated
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    dispatchEvent: vi.fn(),
  })),
});

// --- Polyfill for File constructor and prototype ---
// The `File` object in JSDOM is incomplete. It lacks `arrayBuffer` and its constructor
// can be unreliable across different Node versions. This polyfill ensures a consistent
// `File` object is available in all unit tests, resolving `instanceof` issues and
// errors where the `name` property is missing.
if (typeof global.File === 'undefined') {
  // Create a simplified mock of the File class that has the properties our app uses.
  // This avoids the complex and error-prone type mismatch between JSDOM's Blob and Node's Blob.
  class MockFile {
    name: string;
    lastModified: number;
    size: number;
    type: string;

    constructor(fileBits: BlobPart[], fileName: string, options?: FilePropertyBag) {
      this.name = fileName;
      this.lastModified = options?.lastModified ?? Date.now();
      // FIX: Correctly calculate size by handling all BlobPart types.
      // `ArrayBuffer` and `ArrayBufferView` have `byteLength`, while `Blob` has `size`.
      this.size = fileBits.reduce((acc, part) => {
        if (typeof part === 'string') {
          return acc + part.length;
        }
        // `part.size` exists on `Blob`, while `part.byteLength` exists on `ArrayBuffer` and `ArrayBufferView`.
        return acc + ((part as Blob).size ?? (part as ArrayBuffer).byteLength);
      }, 0);
      this.type = options?.type ?? '';
    }
  }
  vi.stubGlobal('File', MockFile);
}

// --- Polyfill for crypto.subtle ---
// JSDOM environments do not include the 'crypto' module. We need to polyfill it
// for utilities like `generateFileChecksum` to work in tests.
import { webcrypto } from 'node:crypto';

if (!global.crypto) {
  // Use `vi.stubGlobal` which correctly handles read-only properties.
  vi.stubGlobal('crypto', webcrypto);
}

// --- Polyfill for File.prototype.arrayBuffer ---
// The `File` object in JSDOM does not have the `arrayBuffer` method, which is used
// by utilities like `generateFileChecksum` and `pdfConverter`. This polyfill adds it.
if (typeof File.prototype.arrayBuffer === 'undefined') {
  File.prototype.arrayBuffer = function() {
    return new Promise((resolve) => {
      const fr = new FileReader();
      fr.onload = () => {
        // Ensure the result is an ArrayBuffer before resolving.
        resolve(fr.result as ArrayBuffer);
      };
      fr.readAsArrayBuffer(this);
    });
  };
}

// Automatically run cleanup after each test case (e.g., clearing jsdom)
// This is specific to our jsdom-based unit tests.
afterEach(cleanup);

// --- Global Mocks for Unit Tests ---
// By placing mocks here, they are guaranteed to be hoisted and applied
// before any test files are executed, preventing initialization errors.

// --- Global Mocks ---

// 1. Define the mock pool instance logic OUTSIDE the factory so it can be used
// by the factory. However, we define the Constructor function INSIDE the factory
// or via a standard function expression to ensure it remains constructible.
const { mockPoolInstance } = vi.hoisted(() => {
  //console.log('[DEBUG] tests-setup-unit.ts: Initializing hoisted mock variables');
  const mockQuery = vi.fn().mockResolvedValue({ rows: [], rowCount: 0 });
  const mockRelease = vi.fn();
  const mockConnect = vi.fn().mockResolvedValue({
    query: mockQuery,
    release: mockRelease,
  });

  const instance = {
    connect: mockConnect,
    query: mockQuery,
    end: vi.fn(),
    on: vi.fn(),
    totalCount: 10,
    idleCount: 5,
    waitingCount: 0,
  };

  return { mockPoolInstance: instance };
});

// Expose the mock instance globally so it can be imported by tests if needed, 
// though typically they should import from 'pg' directly.
export { mockPoolInstance };

/**
 * Mocks the `pg` module globally.
 * We define the MockPool function INSIDE the factory to ensure it remains a standard function.
 */
vi.mock('pg', () => {
  //console.log('[DEBUG] tests-setup-unit.ts: vi.mock("pg") factory executing');
  
  // FIX: Use a standard function expression for the Mock Pool Constructor.
  // This ensures `new Pool()` works at runtime.
  const MockPool = function() {
    //console.log('[DEBUG] tests-setup-unit.ts: MockPool constructor called via "new"!');
    return mockPoolInstance;
  };

  return {
    // Named export must be the constructor
    Pool: MockPool,
    // Default export often contains Pool as a property
    default: { Pool: MockPool },
    // FIX: Add the `types` object with `builtins` to the mock.
    // This prevents `TypeError: Cannot read properties of undefined (reading 'NUMERIC')`
    // when `connection.db.ts` is imported during tests.
    types: {
      builtins: { NUMERIC: 1700, INT8: 20 }, // Mocked enum values
      setTypeParser: vi.fn(),
    },
  };
});

/**
 * Mocks the Google Generative AI package.
 */
vi.mock('@google/genai', () => {
  const mockGenerativeModel = {
    generateContent: vi.fn().mockResolvedValue({
      // The new SDK structure is slightly different
      text: 'Mocked AI response',
      candidates: [{ content: { parts: [{ text: 'Mocked AI response' }] } }],
    }),
  };
  return {
    // Mock the GoogleGenAI class constructor
    GoogleGenAI: vi.fn(function() {
      return {
        getGenerativeModel: () => mockGenerativeModel,
        models: mockGenerativeModel, // Also mock the `models` getter
      };
    }),
  };
});

/**
 * Mocks the entire apiClient module.
 * This ensures that all test files that import from apiClient will get this mocked version.
 */
vi.mock('../../services/apiClient', () => ({
  // --- Auth ---
  registerUser: vi.fn(),
  loginUser: vi.fn(),
  getAuthenticatedUserProfile: vi.fn(),
  requestPasswordReset: vi.fn(),
  resetPassword: vi.fn(),
  updateUserPassword: vi.fn(),
  deleteUserAccount: vi.fn(),
  updateUserPreferences: vi.fn(),
  updateUserProfile: vi.fn(),
  // --- Data Fetching & Manipulation ---
  fetchFlyers: vi.fn(),
  fetchFlyerItems: vi.fn(),
  // Provide a default implementation that returns a valid Response object to prevent timeouts.
  fetchFlyerItemsForFlyers: vi.fn(() => Promise.resolve(new Response(JSON.stringify([])))),
  countFlyerItemsForFlyers: vi.fn(() => Promise.resolve(new Response(JSON.stringify({ count: 0 })))),
  fetchMasterItems: vi.fn(),
  fetchWatchedItems: vi.fn(),
  addWatchedItem: vi.fn(),
  removeWatchedItem: vi.fn(),
  fetchShoppingLists: vi.fn(),
  createShoppingList: vi.fn(),
  deleteShoppingList: vi.fn(),
  addShoppingListItem: vi.fn(),
  updateShoppingListItem: vi.fn(),
  removeShoppingListItem: vi.fn(),
  fetchHistoricalPriceData: vi.fn(),
  processFlyerFile: vi.fn(),
  uploadLogoAndUpdateStore: vi.fn(),
  exportUserData: vi.fn(),
  // --- Address ---
  getUserAddress: vi.fn(),
  updateUserAddress: vi.fn(),
  geocodeAddress: vi.fn(() => Promise.resolve(new Response(JSON.stringify({ lat: 0, lng: 0 })))),
  // --- Admin ---
  getSuggestedCorrections: vi.fn(),
  fetchCategories: vi.fn(),
  approveCorrection: vi.fn(),
  rejectCorrection: vi.fn(),
  updateSuggestedCorrection: vi.fn(),
  getApplicationStats: vi.fn(),
  fetchActivityLog: vi.fn(),
  fetchAllBrands: vi.fn(),
  uploadBrandLogo: vi.fn(),
  // --- System ---
  pingBackend: vi.fn(),
  checkDbSchema: vi.fn(),
  checkStorage: vi.fn(),
  checkDbPoolHealth: vi.fn(),
  checkRedisHealth: vi.fn(),
  checkPm2Status: vi.fn(),
  fetchLeaderboard: vi.fn(),
}));

// FIX: Mock the aiApiClient module as well, which is used by AnalysisPanel
vi.mock('../../services/aiApiClient', () => ({
  // Provide a default implementation that returns a valid Response object to prevent timeouts.
  uploadAndProcessFlyer: vi.fn(() => Promise.resolve(new Response(JSON.stringify({ jobId: 'mock-job-id' })))),
  isImageAFlyer: vi.fn(),
  extractAddressFromImage: vi.fn(),
  extractLogoFromImage: vi.fn(),
  getQuickInsights: vi.fn(),
  getDeepDiveAnalysis: vi.fn(),
  searchWeb: vi.fn(),  
  planTripWithMaps: vi.fn(),
  generateImageFromText: vi.fn(),
  generateSpeechFromText: vi.fn(),
  startVoiceSession: vi.fn(),
  rescanImageArea: vi.fn(() => Promise.resolve(new Response(JSON.stringify({ text: 'mocked text' })))),
}));

/**
 * Mocks the Express adapter for Bull Board.
 * This is critical for any test that imports `admin.routes.ts`. It replaces the
 * actual Bull Board UI adapter with a lightweight fake. This prevents the test
 * suite from crashing when trying to initialize the real UI, which has complex
 * dependencies not suitable for a test environment.
 */
vi.mock('@bull-board/express', () => ({
  ExpressAdapter: class {
    setBasePath() {}
    getRouter() { return (req: Request, res: Response, next: NextFunction) => next(); }
  },
}));

/**
 * Mocks the logger.
 */
vi.mock('../../services/logger.client', () => ({
  logger: {
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn(),
  },
}));

/**
 * Mocks the notification service.
 */
vi.mock('../../services/notificationService', () => ({
  notifySuccess: vi.fn(),
  notifyError: vi.fn(),
}));

/**
 * Mocks react-hot-toast.
 */
vi.mock('react-hot-toast', () => ({
  default: {
    success: vi.fn(),
    error: vi.fn(),
    loading: vi.fn(() => 'id'),
    dismiss: vi.fn(),
  },
  Toaster: () => null,
}));

// --- Database Service Mocks ---

vi.mock('../../services/db/user.db', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../../services/db/user.db')>();
  return {
    ...actual,
    findUserByEmail: vi.fn(),
    createUser: vi.fn(),
    findUserById: vi.fn(),
    findUserWithPasswordHashById: vi.fn(),
    findUserProfileById: vi.fn(),
    updateUserProfile: vi.fn(),
    updateUserPreferences: vi.fn(),
    updateUserPassword: vi.fn(),
    deleteUserById: vi.fn(),
    saveRefreshToken: vi.fn(),
    findUserByRefreshToken: vi.fn(),
    createPasswordResetToken: vi.fn(),
    getValidResetTokens: vi.fn(),
    deleteResetToken: vi.fn(),
    exportUserData: vi.fn(),
    followUser: vi.fn(),
    unfollowUser: vi.fn(),
    getUserFeed: vi.fn(),
    logSearchQuery: vi.fn(),
    resetFailedLoginAttempts: vi.fn(),
    getAddressById: vi.fn(),
    upsertAddress: vi.fn(),
  };
});

vi.mock('../../services/db/budget.db', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../../services/db/budget.db')>();
  return {
    ...actual,
    getBudgetsForUser: vi.fn().mockResolvedValue([]),
    createBudget: vi.fn(),
    updateBudget: vi.fn(),
    deleteBudget: vi.fn(),
    getSpendingByCategory: vi.fn().mockResolvedValue([]),
  };
});

vi.mock('../../services/db/gamification.db', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../../services/db/gamification.db')>();
  return {
    ...actual,
    getAllAchievements: vi.fn().mockResolvedValue([]),
    getUserAchievements: vi.fn().mockResolvedValue([]),
    awardAchievement: vi.fn(),
    getLeaderboard: vi.fn().mockResolvedValue([]),
  };
});

vi.mock('../../services/db/notification.db', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../../services/db/notification.db')>();
  return {
    ...actual,
    createNotification: vi.fn(),
    createBulkNotifications: vi.fn(),
    getNotificationsForUser: vi.fn().mockResolvedValue([]),
    markAllNotificationsAsRead: vi.fn(),
    markNotificationAsRead: vi.fn(),
  };
});

// --- Server-Side Service Mocks ---

vi.mock('../../services/aiService.server', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../../services/aiService.server')>();
  return {
    ...actual,
    // The singleton instance is named `aiService`. We mock the methods on it.
    aiService: {
      ...actual.aiService, // Spread original methods in case new ones are added
    extractItemsFromReceiptImage: vi.fn().mockResolvedValue([
      { raw_item_description: 'Mock Receipt Item', price_paid_cents: 100 },
    ]),
    extractCoreDataFromFlyerImage: vi.fn().mockResolvedValue({
      store_name: 'Mock Store',
      valid_from: '2023-01-01',
      valid_to: '2023-01-07',
      store_address: '123 Mock St',
      items: [
        { item: 'Mock Apple', price_display: '$1.00', price_in_cents: 100, quantity: '1 lb', category_name: 'Produce', master_item_id: undefined },
      ],
    }),
    extractTextFromImageArea: vi.fn().mockImplementation((path, mime, crop, type) => {
      if (type === 'address') return Promise.resolve({ text: '123 AI Street, Server City' });
      return Promise.resolve({ text: 'Mocked Extracted Text' });
    }),
    planTripWithMaps: vi.fn().mockResolvedValue({
      text: 'Mocked trip plan.',
      sources: [{ uri: 'http://maps.google.com/mock', title: 'Mock Map' }],
    }),
    },
  };
});