// src/tests/e2e/inventory-journey.e2e.test.ts
/**
 * End-to-End test for the Inventory/Expiry management user journey.
 * Tests the complete flow from adding inventory items to tracking expiry and alerts.
 */
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';

/**
 * @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> = {
    'Content-Type': 'application/json',
    ...(fetchOptions.headers as Record<string, string>),
  };

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

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

describe('E2E Inventory/Expiry Management Journey', () => {
  const uniqueId = Date.now();
  const userEmail = `inventory-e2e-${uniqueId}@example.com`;
  const userPassword = 'StrongInventoryPassword123!';

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

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

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

    // 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 user alert settings (expiry_alerts table)
    if (userId) {
      await pool.query('DELETE FROM public.expiry_alerts WHERE user_id = $1', [userId]);
    }

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

  it('should complete inventory journey: Register -> Add Items -> Track Expiry -> Consume -> Configure Alerts', async () => {
    // Step 1: Register a new user
    const registerResponse = await apiClient.registerUser(
      userEmail,
      userPassword,
      'Inventory 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();

    // Calculate dates for testing
    const today = new Date();
    const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
    const nextWeek = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000);
    const nextMonth = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000);
    const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);

    const formatDate = (d: Date) => d.toISOString().split('T')[0];

    // Step 3: Add multiple inventory items with different expiry dates
    // Note: API requires 'source' field (manual, receipt_scan, upc_scan)
    // Also: pantry_items table requires master_item_id, so we need to create master items first
    const pool = getPool();

    // Create master grocery items for our test items
    const masterItemNames = ['E2E Milk', 'E2E Frozen Pizza', 'E2E Bread', 'E2E Apples', 'E2E Rice'];
    const masterItemIds: number[] = [];

    for (const name of masterItemNames) {
      const result = await pool.query(
        `INSERT INTO public.master_grocery_items (name)
         VALUES ($1)
         ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
         RETURNING master_grocery_item_id`,
        [name],
      );
      masterItemIds.push(result.rows[0].master_grocery_item_id);
    }

    const items = [
      {
        item_name: 'E2E Milk',
        master_item_id: masterItemIds[0],
        quantity: 2,
        location: 'fridge',
        expiry_date: formatDate(tomorrow),
        source: 'manual',
      },
      {
        item_name: 'E2E Frozen Pizza',
        master_item_id: masterItemIds[1],
        quantity: 3,
        location: 'freezer',
        expiry_date: formatDate(nextMonth),
        source: 'manual',
      },
      {
        item_name: 'E2E Bread',
        master_item_id: masterItemIds[2],
        quantity: 1,
        location: 'pantry',
        expiry_date: formatDate(nextWeek),
        source: 'manual',
      },
      {
        item_name: 'E2E Apples',
        master_item_id: masterItemIds[3],
        quantity: 6,
        location: 'fridge',
        expiry_date: formatDate(nextWeek),
        source: 'manual',
      },
      {
        item_name: 'E2E Rice',
        master_item_id: masterItemIds[4],
        quantity: 1,
        location: 'pantry',
        source: 'manual',
        // No expiry date - non-perishable
      },
    ];

    for (const item of items) {
      const addResponse = await authedFetch('/inventory', {
        method: 'POST',
        token: authToken,
        body: JSON.stringify(item),
      });

      expect(addResponse.status).toBe(201);
      const addData = await addResponse.json();
      expect(addData.data.item_name).toBe(item.item_name);
      createdInventoryIds.push(addData.data.inventory_id);
    }

    // Add an expired item directly to the database for testing expired endpoint
    // First create a master_grocery_item and pantry_location for the direct insert
    // (pool already defined above)

    // Create or get the master grocery item
    const masterItemResult = await pool.query(
      `INSERT INTO public.master_grocery_items (name)
       VALUES ('Expired Yogurt E2E')
       ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
       RETURNING master_grocery_item_id`,
    );
    const masterItemId = masterItemResult.rows[0].master_grocery_item_id;

    // Create or get the pantry location
    const locationResult = await pool.query(
      `INSERT INTO public.pantry_locations (user_id, name)
       VALUES ($1, 'fridge')
       ON CONFLICT (user_id, name) DO UPDATE SET name = EXCLUDED.name
       RETURNING pantry_location_id`,
      [userId],
    );
    const pantryLocationId = locationResult.rows[0].pantry_location_id;

    // Insert the expired pantry item
    const expiredResult = await pool.query(
      `INSERT INTO public.pantry_items (user_id, master_item_id, quantity, pantry_location_id, best_before_date, source)
       VALUES ($1, $2, 1, $3, $4, 'manual')
       RETURNING pantry_item_id`,
      [userId, masterItemId, pantryLocationId, formatDate(yesterday)],
    );
    createdInventoryIds.push(expiredResult.rows[0].pantry_item_id);

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

    expect(listResponse.status).toBe(200);
    const listData = await listResponse.json();
    expect(listData.data.items.length).toBe(6); // All our items
    expect(listData.data.total).toBe(6);

    // Step 5: Filter by location
    const fridgeResponse = await authedFetch('/inventory?location=fridge', {
      method: 'GET',
      token: authToken,
    });

    expect(fridgeResponse.status).toBe(200);
    const fridgeData = await fridgeResponse.json();
    fridgeData.data.items.forEach((item: { location: string }) => {
      expect(item.location).toBe('fridge');
    });
    expect(fridgeData.data.items.length).toBe(3); // Milk, Apples, Expired Yogurt

    // Step 6: View expiring items
    const expiringResponse = await authedFetch('/inventory/expiring?days=3', {
      method: 'GET',
      token: authToken,
    });

    expect(expiringResponse.status).toBe(200);
    const expiringData = await expiringResponse.json();
    // Should include the Milk (tomorrow)
    expect(expiringData.data.items.length).toBeGreaterThanOrEqual(1);

    // Step 7: View expired items
    const expiredResponse = await authedFetch('/inventory/expired', {
      method: 'GET',
      token: authToken,
    });

    expect(expiredResponse.status).toBe(200);
    const expiredData = await expiredResponse.json();
    expect(expiredData.data.items.length).toBeGreaterThanOrEqual(1);

    // Find the expired yogurt
    const expiredYogurt = expiredData.data.items.find(
      (i: { item_name: string }) => i.item_name === 'Expired Yogurt E2E',
    );
    expect(expiredYogurt).toBeDefined();

    // Step 8: Get specific item details
    const milkId = createdInventoryIds[0];
    const detailResponse = await authedFetch(`/inventory/${milkId}`, {
      method: 'GET',
      token: authToken,
    });

    expect(detailResponse.status).toBe(200);
    const detailData = await detailResponse.json();
    expect(detailData.data.item_name).toBe('E2E Milk');
    expect(detailData.data.quantity).toBe(2);

    // Step 9: Update item quantity and location
    const updateResponse = await authedFetch(`/inventory/${milkId}`, {
      method: 'PUT',
      token: authToken,
      body: JSON.stringify({
        quantity: 1,
        notes: 'One bottle used',
      }),
    });

    expect(updateResponse.status).toBe(200);
    const updateData = await updateResponse.json();
    expect(updateData.data.quantity).toBe(1);

    // Step 10: Consume some apples (partial consume via update, then mark fully consumed)
    // First, reduce quantity via update
    const applesId = createdInventoryIds[3];
    const partialConsumeResponse = await authedFetch(`/inventory/${applesId}`, {
      method: 'PUT',
      token: authToken,
      body: JSON.stringify({ quantity: 4 }), // 6 - 2 = 4
    });

    expect(partialConsumeResponse.status).toBe(200);
    const partialConsumeData = await partialConsumeResponse.json();
    expect(partialConsumeData.data.quantity).toBe(4);

    // Step 11: Configure alert settings for email
    // The API uses PUT /inventory/alerts/:alertMethod with days_before_expiry and is_enabled
    const alertSettingsResponse = await authedFetch('/inventory/alerts/email', {
      method: 'PUT',
      token: authToken,
      body: JSON.stringify({
        is_enabled: true,
        days_before_expiry: 3,
      }),
    });

    expect(alertSettingsResponse.status).toBe(200);
    const alertSettingsData = await alertSettingsResponse.json();
    expect(alertSettingsData.data.is_enabled).toBe(true);
    expect(alertSettingsData.data.days_before_expiry).toBe(3);

    // Step 12: Verify alert settings were saved
    const getSettingsResponse = await authedFetch('/inventory/alerts', {
      method: 'GET',
      token: authToken,
    });

    expect(getSettingsResponse.status).toBe(200);
    const getSettingsData = await getSettingsResponse.json();
    // Should have email alerts enabled
    const emailAlert = getSettingsData.data.find(
      (s: { alert_method: string }) => s.alert_method === 'email',
    );
    expect(emailAlert?.is_enabled).toBe(true);

    // Step 13: Get recipe suggestions based on expiring items
    const suggestionsResponse = await authedFetch('/inventory/recipes/suggestions', {
      method: 'GET',
      token: authToken,
    });

    expect(suggestionsResponse.status).toBe(200);
    const suggestionsData = await suggestionsResponse.json();
    expect(Array.isArray(suggestionsData.data.recipes)).toBe(true);

    // Step 14: Fully consume an item (marks as consumed, returns 204)
    const breadId = createdInventoryIds[2];
    const fullConsumeResponse = await authedFetch(`/inventory/${breadId}/consume`, {
      method: 'POST',
      token: authToken,
    });

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

    // Verify the item is now marked as consumed
    const consumedItemResponse = await authedFetch(`/inventory/${breadId}`, {
      method: 'GET',
      token: authToken,
    });
    expect(consumedItemResponse.status).toBe(200);
    const consumedItemData = await consumedItemResponse.json();
    expect(consumedItemData.data.is_consumed).toBe(true);

    // Step 15: Delete an item
    const riceId = createdInventoryIds[4];
    const deleteResponse = await authedFetch(`/inventory/${riceId}`, {
      method: 'DELETE',
      token: authToken,
    });

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

    // Remove from tracking list
    const deleteIndex = createdInventoryIds.indexOf(riceId);
    if (deleteIndex > -1) {
      createdInventoryIds.splice(deleteIndex, 1);
    }

    // Step 16: Verify deletion
    const verifyDeleteResponse = await authedFetch(`/inventory/${riceId}`, {
      method: 'GET',
      token: authToken,
    });

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

    // Step 17: Verify another user cannot access our inventory
    const otherUserEmail = `other-inventory-e2e-${uniqueId}@example.com`;
    await apiClient.registerUser(otherUserEmail, userPassword, 'Other Inventory 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 inventory
    const otherDetailResponse = await authedFetch(`/inventory/${milkId}`, {
      method: 'GET',
      token: otherToken,
    });

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

    // Other user's inventory should be empty
    const otherListResponse = await authedFetch('/inventory', {
      method: 'GET',
      token: otherToken,
    });

    expect(otherListResponse.status).toBe(200);
    const otherListData = await otherListResponse.json();
    expect(otherListData.data.total).toBe(0);

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

    // Step 18: Move frozen item to fridge (simulating thawing)
    const pizzaId = createdInventoryIds[1];
    const moveResponse = await authedFetch(`/inventory/${pizzaId}`, {
      method: 'PUT',
      token: authToken,
      body: JSON.stringify({
        location: 'fridge',
        expiry_date: formatDate(nextWeek), // Update expiry since thawed
        notes: 'Thawed for dinner',
      }),
    });

    expect(moveResponse.status).toBe(200);
    const moveData = await moveResponse.json();
    expect(moveData.data.location).toBe('fridge');

    // Step 19: Final inventory check
    const finalListResponse = await authedFetch('/inventory', {
      method: 'GET',
      token: authToken,
    });

    expect(finalListResponse.status).toBe(200);
    const finalListData = await finalListResponse.json();
    // We should have: Milk (1), Pizza (thawed, 3), Bread (consumed), Apples (4), Expired Yogurt (1)
    // Rice was deleted, Bread was consumed
    expect(finalListData.data.total).toBeLessThanOrEqual(5);

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

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