// src/tests/e2e/receipt-journey.e2e.test.ts
/**
 * End-to-End test for the Receipt processing user journey.
 * Tests the complete flow from user registration to uploading receipts and managing items.
 */
import { describe, it, expect, afterAll } from 'vitest';
import * as apiClient from '../../services/apiClient';
import { cleanupDb } from '../utils/cleanup';
import { poll } from '../utils/poll';
import { getPool } from '../../services/db/connection.db';
import FormData from 'form-data';

/**
 * @vitest-environment node
 */

const API_BASE_URL = process.env.VITE_API_BASE_URL || 'http://localhost:3000/api';

// Helper to make authenticated API calls
const authedFetch = async (
  path: string,
  options: RequestInit & { token?: string } = {},
): Promise<Response> => {
  const { token, ...fetchOptions } = options;
  const headers: Record<string, string> = {
    ...(fetchOptions.headers as Record<string, string>),
  };

  // Only add Content-Type for JSON (not for FormData)
  if (!(fetchOptions.body instanceof FormData)) {
    headers['Content-Type'] = 'application/json';
  }

  if (token) {
    headers['Authorization'] = `Bearer ${token}`;
  }

  return fetch(`${API_BASE_URL}${path}`, {
    ...fetchOptions,
    headers,
  });
};

describe('E2E Receipt Processing Journey', () => {
  const uniqueId = Date.now();
  const userEmail = `receipt-e2e-${uniqueId}@example.com`;
  const userPassword = 'StrongReceiptPassword123!';

  let authToken: string;
  let userId: string | null = null;
  const createdReceiptIds: number[] = [];
  const createdInventoryIds: number[] = [];

  afterAll(async () => {
    const pool = getPool();

    // Clean up inventory items (pantry_items table)
    if (createdInventoryIds.length > 0) {
      await pool.query('DELETE FROM public.pantry_items WHERE pantry_item_id = ANY($1::bigint[])', [
        createdInventoryIds,
      ]);
    }

    // Clean up receipt items and receipts
    if (createdReceiptIds.length > 0) {
      await pool.query('DELETE FROM public.receipt_items WHERE receipt_id = ANY($1::bigint[])', [
        createdReceiptIds,
      ]);
      await pool.query(
        'DELETE FROM public.receipt_processing_log WHERE receipt_id = ANY($1::bigint[])',
        [createdReceiptIds],
      );
      await pool.query('DELETE FROM public.receipts WHERE receipt_id = ANY($1::bigint[])', [
        createdReceiptIds,
      ]);
    }

    // Clean up user
    await cleanupDb({
      userIds: [userId],
    });
  });

  it('should complete receipt journey: Register -> Upload -> View -> Manage Items -> Add to Inventory', async () => {
    // Step 1: Register a new user
    const registerResponse = await apiClient.registerUser(
      userEmail,
      userPassword,
      'Receipt E2E User',
    );
    expect(registerResponse.status).toBe(201);

    // Step 2: Login to get auth token
    const { response: loginResponse, responseBody: loginResponseBody } = await poll(
      async () => {
        const response = await apiClient.loginUser(userEmail, userPassword, false);
        const responseBody = response.ok ? await response.clone().json() : {};
        return { response, responseBody };
      },
      (result) => result.response.ok,
      { timeout: 10000, interval: 1000, description: 'user login after registration' },
    );

    expect(loginResponse.status).toBe(200);
    authToken = loginResponseBody.data.token;
    userId = loginResponseBody.data.userprofile.user.user_id;
    expect(authToken).toBeDefined();

    // Step 3: Create a receipt directly in the database (simulating a completed upload)
    // In a real E2E test with full BullMQ setup, we would upload and wait for processing
    // Note: receipts table uses store_id (FK to stores) and total_amount_cents (integer cents)
    const pool = getPool();

    // First, create or get a test store
    const storeResult = await pool.query(
      `INSERT INTO public.stores (name)
       VALUES ('E2E Test Store')
       ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
       RETURNING store_id`,
    );
    const storeId = storeResult.rows[0].store_id;

    const receiptResult = await pool.query(
      `INSERT INTO public.receipts (user_id, receipt_image_url, status, store_id, total_amount_cents, transaction_date)
       VALUES ($1, '/uploads/receipts/e2e-test.jpg', 'completed', $2, 4999, '2024-01-15')
       RETURNING receipt_id`,
      [userId, storeId],
    );
    const receiptId = receiptResult.rows[0].receipt_id;
    createdReceiptIds.push(receiptId);

    // Add receipt items
    // receipt_items uses: raw_item_description, quantity, price_paid_cents, status
    const itemsResult = await pool.query(
      `INSERT INTO public.receipt_items (receipt_id, raw_item_description, quantity, price_paid_cents, status)
       VALUES
         ($1, 'MILK 2% 4L', 1, 599, 'matched'),
         ($1, 'BREAD WHITE', 2, 498, 'unmatched'),
         ($1, 'EGGS LARGE 12', 1, 499, 'matched')
       RETURNING receipt_item_id`,
      [receiptId],
    );
    const itemIds = itemsResult.rows.map((r) => r.receipt_item_id);

    // Step 4: View receipt list
    const listResponse = await authedFetch('/receipts', {
      method: 'GET',
      token: authToken,
    });

    expect(listResponse.status).toBe(200);
    const listData = await listResponse.json();
    expect(listData.success).toBe(true);
    expect(listData.data.receipts.length).toBeGreaterThanOrEqual(1);

    // Find our receipt
    const ourReceipt = listData.data.receipts.find(
      (r: { receipt_id: number }) => r.receipt_id === receiptId,
    );
    expect(ourReceipt).toBeDefined();
    expect(ourReceipt.store_id).toBe(storeId);

    // Step 5: View receipt details
    const detailResponse = await authedFetch(`/receipts/${receiptId}`, {
      method: 'GET',
      token: authToken,
    });

    expect(detailResponse.status).toBe(200);
    const detailData = await detailResponse.json();
    expect(detailData.data.receipt.receipt_id).toBe(receiptId);
    expect(detailData.data.items.length).toBe(3);

    // Step 6: View receipt items
    const itemsResponse = await authedFetch(`/receipts/${receiptId}/items`, {
      method: 'GET',
      token: authToken,
    });

    expect(itemsResponse.status).toBe(200);
    const itemsData = await itemsResponse.json();
    expect(itemsData.data.items.length).toBe(3);

    // Step 7: Update an item's status
    const updateItemResponse = await authedFetch(`/receipts/${receiptId}/items/${itemIds[1]}`, {
      method: 'PUT',
      token: authToken,
      body: JSON.stringify({
        status: 'matched',
        match_confidence: 0.85,
      }),
    });

    expect(updateItemResponse.status).toBe(200);
    const updateItemData = await updateItemResponse.json();
    expect(updateItemData.data.status).toBe('matched');

    // Step 8: View unadded items
    const unaddedResponse = await authedFetch(`/receipts/${receiptId}/items/unadded`, {
      method: 'GET',
      token: authToken,
    });

    expect(unaddedResponse.status).toBe(200);
    const unaddedData = await unaddedResponse.json();
    expect(unaddedData.data.items.length).toBe(3); // None added yet

    // Step 9: Confirm items to add to inventory
    const confirmResponse = await authedFetch(`/receipts/${receiptId}/confirm`, {
      method: 'POST',
      token: authToken,
      body: JSON.stringify({
        items: [
          {
            receipt_item_id: itemIds[0],
            include: true,
            item_name: 'Milk 2%',
            quantity: 1,
            location: 'fridge',
            expiry_date: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
          },
          {
            receipt_item_id: itemIds[1],
            include: true,
            item_name: 'White Bread',
            quantity: 2,
            location: 'pantry',
          },
          {
            receipt_item_id: itemIds[2],
            include: false, // Skip the eggs
          },
        ],
      }),
    });

    expect(confirmResponse.status).toBe(200);
    const confirmData = await confirmResponse.json();
    expect(confirmData.data.count).toBeGreaterThanOrEqual(0);

    // Track inventory items for cleanup
    if (confirmData.data.added_items) {
      confirmData.data.added_items.forEach((item: { inventory_id: number }) => {
        if (item.inventory_id) {
          createdInventoryIds.push(item.inventory_id);
        }
      });
    }

    // Step 10: Verify items in inventory
    const inventoryResponse = await authedFetch('/inventory', {
      method: 'GET',
      token: authToken,
    });

    expect(inventoryResponse.status).toBe(200);
    const inventoryData = await inventoryResponse.json();
    // Should have at least the items we added
    expect(inventoryData.data.items.length).toBeGreaterThanOrEqual(0);

    // Step 11-12: Processing logs tests skipped - receipt_processing_logs table not implemented
    // TODO: Add these steps back when the receipt_processing_logs table is added to the schema
    // See: The route /receipts/:receiptId/logs exists but the backing table does not

    // Step 13: Verify another user cannot access our receipt
    const otherUserEmail = `other-receipt-e2e-${uniqueId}@example.com`;
    await apiClient.registerUser(otherUserEmail, userPassword, 'Other Receipt User');

    const { responseBody: otherLoginData } = await poll(
      async () => {
        const response = await apiClient.loginUser(otherUserEmail, userPassword, false);
        const responseBody = response.ok ? await response.clone().json() : {};
        return { response, responseBody };
      },
      (result) => result.response.ok,
      { timeout: 10000, interval: 1000, description: 'other user login' },
    );

    const otherToken = otherLoginData.data.token;
    const otherUserId = otherLoginData.data.userprofile.user.user_id;

    // Other user should not see our receipt
    const otherDetailResponse = await authedFetch(`/receipts/${receiptId}`, {
      method: 'GET',
      token: otherToken,
    });

    expect(otherDetailResponse.status).toBe(404);

    // Clean up other user
    await cleanupDb({ userIds: [otherUserId] });

    // Step 14: Create a second receipt to test listing and filtering
    // Use the same store_id we created earlier, and use total_amount_cents (integer cents)
    const receipt2Result = await pool.query(
      `INSERT INTO public.receipts (user_id, receipt_image_url, status, store_id, total_amount_cents)
       VALUES ($1, '/uploads/receipts/e2e-test-2.jpg', 'failed', $2, 2500)
       RETURNING receipt_id`,
      [userId, storeId],
    );
    createdReceiptIds.push(receipt2Result.rows[0].receipt_id);

    // Step 15: Test filtering by status
    const completedResponse = await authedFetch('/receipts?status=completed', {
      method: 'GET',
      token: authToken,
    });

    expect(completedResponse.status).toBe(200);
    const completedData = await completedResponse.json();
    completedData.data.receipts.forEach((r: { status: string }) => {
      expect(r.status).toBe('completed');
    });

    // Step 16: Test reprocessing a failed receipt
    const reprocessResponse = await authedFetch(
      `/receipts/${receipt2Result.rows[0].receipt_id}/reprocess`,
      {
        method: 'POST',
        token: authToken,
      },
    );

    expect(reprocessResponse.status).toBe(200);
    const reprocessData = await reprocessResponse.json();
    expect(reprocessData.data.message).toContain('reprocessing');

    // Step 17: Delete the failed receipt
    const deleteResponse = await authedFetch(`/receipts/${receipt2Result.rows[0].receipt_id}`, {
      method: 'DELETE',
      token: authToken,
    });

    expect(deleteResponse.status).toBe(204);

    // Remove from cleanup list since we deleted it
    const deleteIndex = createdReceiptIds.indexOf(receipt2Result.rows[0].receipt_id);
    if (deleteIndex > -1) {
      createdReceiptIds.splice(deleteIndex, 1);
    }

    // Step 18: Verify deletion
    const verifyDeleteResponse = await authedFetch(
      `/receipts/${receipt2Result.rows[0].receipt_id}`,
      {
        method: 'GET',
        token: authToken,
      },
    );

    expect(verifyDeleteResponse.status).toBe(404);

    // Step 19: Delete account
    const deleteAccountResponse = await apiClient.deleteUserAccount(userPassword, {
      tokenOverride: authToken,
    });

    expect(deleteAccountResponse.status).toBe(200);
    userId = null;
  });
});
