// src/tests/integration/gamification.integration.test.ts
import { describe, it, expect, beforeAll, afterAll, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import path from 'path';
import fs from 'node:fs/promises';
import { getPool } from '../../services/db/connection.db';
import { createAndLoginUser } from '../utils/testHelpers';
import { generateFileChecksum } from '../../utils/checksum';
import * as db from '../../services/db/index.db';
import { cleanupDb } from '../utils/cleanup';
import { logger } from '../../services/logger.server';
import * as imageProcessor from '../../utils/imageProcessor';
import { poll } from '../utils/poll';
import type { UserProfile, LeaderboardUser } from '../../types';
import type { Flyer } from '../../types';
import { cleanupFiles } from '../utils/cleanupFiles';
import { aiService } from '../../services/aiService.server';

/**
 * @vitest-environment node
 */

const { mockExtractCoreData } = vi.hoisted(() => ({
  mockExtractCoreData: vi.fn(),
}));

// Mock the image processor to control icon generation for legacy uploads
vi.mock('../../utils/imageProcessor', async () => {
  const actual = await vi.importActual<typeof imageProcessor>('../../utils/imageProcessor');
  return {
    ...actual,
    generateFlyerIcon: vi.fn().mockResolvedValue('mock-icon.webp'),
  };
});

describe('Gamification Flow Integration Test', () => {
  let request: ReturnType<typeof supertest>;
  let testUser: UserProfile;
  let authToken: string;
  const createdFlyerIds: number[] = [];
  const createdFilePaths: string[] = [];
  const createdStoreIds: number[] = [];
  let workersModule: typeof import('../../services/workers.server');

  beforeAll(async () => {
    // Stub environment variables for URL generation in the background worker.
    // This needs to be in beforeAll to ensure it's set before any code that might use it is imported.
    vi.stubEnv('FRONTEND_URL', 'https://example.com');

    // Spy on the actual singleton instance. This ensures that when the worker
    // imports 'aiService', it gets the instance we are controlling here.
    vi.spyOn(aiService, 'extractCoreDataFromFlyerImage').mockImplementation(mockExtractCoreData);

    // Import workers to start them IN-PROCESS.
    // This ensures they run in the same memory space as our mocks.
    workersModule = await import('../../services/workers.server');

    const app = (await import('../../../server')).default;
    request = supertest(app);

    // Create a new user specifically for this test suite to ensure a clean slate.
    ({ user: testUser, token: authToken } = await createAndLoginUser({
      email: `gamification-user-${Date.now()}@example.com`,
      fullName: 'Gamification Tester',
      request,
    }));
  });

  beforeEach(() => {
    vi.clearAllMocks();

    // Reset AI Service Mock to default success state
    mockExtractCoreData.mockReset();
    mockExtractCoreData.mockResolvedValue({
      store_name: 'Gamification Test Store',
      valid_from: null,
      valid_to: null,
      store_address: null,
      items: [
        {
          item: 'Integration Test Milk',
          price_display: '$4.99',
          price_in_cents: 499,
          quantity: '2L',
          category_name: 'Dairy',
        },
      ],
    });

    // Reset Image Processor Mock
    vi.mocked(imageProcessor.generateFlyerIcon).mockResolvedValue('mock-icon.webp');
  });

  afterAll(async () => {
    vi.unstubAllEnvs();
    vi.restoreAllMocks(); // Restore the AI spy

    // CRITICAL: Close workers FIRST before any cleanup to ensure no pending jobs
    // are trying to access files or databases during cleanup.
    // This prevents the Node.js async hooks crash that occurs when fs operations
    // are rejected during process shutdown.
    if (workersModule) {
      await workersModule.closeWorkers();
      // Give workers a moment to fully release resources
      await new Promise((resolve) => setTimeout(resolve, 100));
    }

    // Close the shared redis connection used by the workers/queues
    const { connection } = await import('../../services/redis.server');
    await connection.quit();

    await cleanupDb({
      userIds: testUser ? [testUser.user.user_id] : [],
      flyerIds: createdFlyerIds,
      storeIds: createdStoreIds,
    });
    await cleanupFiles(createdFilePaths);

    // Final delay to let any remaining async operations settle
    await new Promise((resolve) => setTimeout(resolve, 50));
  });

  it('should award the "First Upload" achievement after a user successfully uploads and processes their first flyer', async () => {
    // --- Arrange: Prepare a unique flyer file for upload ---
    const imagePath = path.resolve(__dirname, '../assets/test-flyer-image.jpg');
    const imageBuffer = await fs.readFile(imagePath);
    const uniqueContent = Buffer.concat([imageBuffer, Buffer.from(Date.now().toString())]);
    const uniqueFileName = `gamification-test-flyer-${Date.now()}.jpg`;
    const mockImageFile = new File([new Uint8Array(uniqueContent)], uniqueFileName, {
      type: 'image/jpeg',
    });
    const checksum = await generateFileChecksum(mockImageFile);

    // Track created files for cleanup
    const uploadDir = path.resolve(__dirname, '../../../flyer-images');
    createdFilePaths.push(path.join(uploadDir, uniqueFileName));
    const iconFileName = `icon-${path.parse(uniqueFileName).name}.webp`;
    createdFilePaths.push(path.join(uploadDir, 'icons', iconFileName));

    // --- Act 1: Upload the flyer to trigger the background job ---
    const testBaseUrl = 'https://example.com';
    console.error(
      '--------------------------------------------------------------------------------',
    );
    console.error('[TEST DEBUG] STARTING UPLOAD STEP');
    console.error(`[TEST DEBUG] Env FRONTEND_URL: "${process.env.FRONTEND_URL}"`);
    console.error(`[TEST DEBUG] Sending baseUrl field: "${testBaseUrl}"`);
    console.error(
      '--------------------------------------------------------------------------------',
    );

    const uploadResponse = await request
      .post('/api/ai/upload-and-process')
      .set('Authorization', `Bearer ${authToken}`)
      .field('checksum', checksum)
      .field('baseUrl', testBaseUrl)
      .attach('flyerFile', uniqueContent, uniqueFileName);

    console.error(
      '--------------------------------------------------------------------------------',
    );
    console.error(`[TEST DEBUG] Upload Response Status: ${uploadResponse.status}`);
    console.error(
      `[TEST DEBUG] Upload Response Body: ${JSON.stringify(uploadResponse.body, null, 2)}`,
    );
    console.error(
      '--------------------------------------------------------------------------------',
    );

    const { jobId } = uploadResponse.body.data;
    expect(jobId).toBeTypeOf('string');
    console.error(`[TEST DEBUG] Job ID received: ${jobId}`);

    // --- Act 2: Poll for job completion using the new utility ---
    const jobStatus = await poll(
      async () => {
        const statusResponse = await request
          .get(`/api/ai/jobs/${jobId}/status`)
          .set('Authorization', `Bearer ${authToken}`);
        console.error(
          `[TEST DEBUG] Polling status for ${jobId}: ${statusResponse.body?.data?.state}`,
        );
        return statusResponse.body.data;
      },
      (status) => status.state === 'completed' || status.state === 'failed',
      { timeout: 180000, interval: 3000, description: 'gamification flyer processing' },
    );

    if (!jobStatus) {
      console.error('[DEBUG] Gamification test job timed out: No job status received.');
      throw new Error('Gamification test job timed out: No job status received.');
    }

    console.error(
      '--------------------------------------------------------------------------------',
    );
    console.error('[TEST DEBUG] Final Job Status Object:', JSON.stringify(jobStatus, null, 2));
    if (jobStatus.state === 'failed') {
      console.error(`[TEST DEBUG] Job Failed Reason: ${jobStatus.failedReason}`);
      // If there is a progress object with error details, log it
      if (jobStatus.progress) {
        console.error(
          `[TEST DEBUG] Job Progress/Error Details:`,
          JSON.stringify(jobStatus.progress, null, 2),
        );
      }
    }
    console.error(
      '--------------------------------------------------------------------------------',
    );

    // --- Assert 1: Verify the job completed successfully ---
    if (jobStatus?.state === 'failed') {
      console.error('[DEBUG] Gamification test job failed:', jobStatus.failedReason);
      console.error('[DEBUG] Job stack trace:', jobStatus.stacktrace);
      console.error('[DEBUG] Job return value:', JSON.stringify(jobStatus.returnValue, null, 2));
    }
    expect(jobStatus?.state).toBe('completed');

    const flyerId = jobStatus?.returnValue?.flyerId;
    expect(flyerId).toBeTypeOf('number');
    createdFlyerIds.push(flyerId); // Track for cleanup

    // --- Assert 1.5: Verify the flyer was saved with the correct original filename ---
    const savedFlyer = await db.flyerRepo.findFlyerByChecksum(checksum, logger);
    expect(savedFlyer).toBeDefined();
    expect(savedFlyer?.file_name).toBe(uniqueFileName);
    if (savedFlyer?.store_id) {
      createdStoreIds.push(savedFlyer.store_id);
    }
    // Also add the final processed image path to the cleanup list.
    // This is important because JPEGs are re-processed to strip EXIF data, creating a new file.
    const savedImagePath = path.join(uploadDir, path.basename(savedFlyer!.image_url));
    createdFilePaths.push(savedImagePath);

    // --- Act 3: Fetch the user's achievements (triggers endpoint, response not needed) ---
    await request.get('/api/achievements/me').set('Authorization', `Bearer ${authToken}`);

    // --- Assert 2: Verify the "First-Upload" achievement was awarded ---
    // The 'Welcome Aboard' achievement is awarded on user creation, so we expect at least two.
    // Wait for the asynchronous achievement event to process
    await vi.waitUntil(
      async () => {
        const achievements = await db.gamificationRepo.getUserAchievements(
          testUser.user.user_id,
          logger,
        );
        return achievements.length >= 2;
      },
      { timeout: 15000, interval: 500 },
    );

    // Final assertion and retrieval
    const userAchievements = await db.gamificationRepo.getUserAchievements(
      testUser.user.user_id,
      logger,
    );
    expect(userAchievements.length).toBeGreaterThanOrEqual(2);
    const firstUploadAchievement = userAchievements.find((ach) => ach.name === 'First-Upload');
    expect(firstUploadAchievement).toBeDefined();
    expect(firstUploadAchievement?.points_value).toBeGreaterThan(0);

    // --- Act 4: Fetch the leaderboard ---
    const leaderboardResponse = await request.get('/api/achievements/leaderboard');
    const leaderboard: LeaderboardUser[] = leaderboardResponse.body.data;

    // --- Assert 3: Verify the user is on the leaderboard with points ---
    const userOnLeaderboard = leaderboard.find((u) => u.user_id === testUser.user.user_id);
    expect(userOnLeaderboard).toBeDefined();
    // The user should have points from 'user_registered' and 'First-Upload'.
    // We check that the points are greater than or equal to the points from the upload achievement.
    expect(Number(userOnLeaderboard?.points)).toBeGreaterThanOrEqual(
      firstUploadAchievement!.points_value,
    );
  }, 240000); // Increase timeout to 240s to match other long-running processing tests

  describe('Legacy Flyer Upload', () => {
    it('should process a legacy upload and save fully qualified URLs to the database', async () => {
      // --- Arrange ---
      // 2. Mock the icon generator to return a predictable filename.
      vi.mocked(imageProcessor.generateFlyerIcon).mockResolvedValue('legacy-icon.webp');

      // 3. Prepare a unique file for upload to avoid checksum conflicts.
      const imagePath = path.resolve(__dirname, '../assets/test-flyer-image.jpg');
      const imageBuffer = await fs.readFile(imagePath);
      const uniqueFileName = `legacy-upload-test-${Date.now()}.jpg`;
      const mockImageFile = new File([new Uint8Array(imageBuffer)], uniqueFileName, {
        type: 'image/jpeg',
      });
      const checksum = await generateFileChecksum(mockImageFile);

      // Track created files for cleanup.
      const uploadDir = path.resolve(__dirname, '../../../flyer-images');
      createdFilePaths.push(path.join(uploadDir, uniqueFileName));
      createdFilePaths.push(path.join(uploadDir, 'icons', 'legacy-icon.webp'));

      // 4. Prepare the legacy payload (body of the request).
      const storeName = `Legacy Store - ${Date.now()}`;
      const legacyPayload = {
        checksum: checksum,
        extractedData: {
          store_name: storeName,
          items: [{ item: 'Legacy Milk', price_in_cents: 250, price_display: '$2.50' }],
        },
      };

      // --- Act ---
      // 5. Make the API request.
      // Note: This assumes a legacy endpoint exists at `/api/ai/upload-legacy`.
      // This endpoint would be responsible for calling `aiService.processLegacyFlyerUpload`.
      const response = await request
        .post('/api/ai/upload-legacy')
        .set('Authorization', `Bearer ${authToken}`)
        .field('data', JSON.stringify(legacyPayload))
        .attach('flyerFile', imageBuffer, uniqueFileName);

      // --- Assert ---
      // 6. Check for a successful response.
      expect(response.status).toBe(200);
      const newFlyer: Flyer = response.body.data;
      expect(newFlyer).toBeDefined();
      expect(newFlyer.flyer_id).toBeTypeOf('number');
      createdFlyerIds.push(newFlyer.flyer_id); // Add for cleanup.

      // 7. Query the database directly to verify the saved values.
      const pool = getPool();
      const dbResult = await pool.query<Flyer>(
        'SELECT image_url, icon_url, store_id FROM public.flyers WHERE flyer_id = $1',
        [newFlyer.flyer_id],
      );

      expect(dbResult.rowCount).toBe(1);
      const savedFlyer = dbResult.rows[0];
      // The store_id is guaranteed to exist for a saved flyer, but the generic `Flyer` type
      // might have it as optional. We use a non-null assertion `!` to satisfy TypeScript.
      createdStoreIds.push(savedFlyer.store_id!); // Add for cleanup.

      // 8. Assert that the URLs are fully qualified.
      expect(savedFlyer.image_url).to.equal(newFlyer.image_url);
      expect(savedFlyer.icon_url).to.equal(newFlyer.icon_url);
      const expectedBaseUrl = 'https://example.com';
      expect(newFlyer.image_url).toContain(`${expectedBaseUrl}/flyer-images/`);
    });
  });
});
