// src/tests/integration/public.routes.integration.test.ts
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import supertest from 'supertest';
import type {
  Flyer,
  FlyerItem,
  Recipe,
  RecipeComment,
  DietaryRestriction,
  Appliance,
  UserProfile,
} from '../../types';
import { getPool } from '../../services/db/connection.db';
import { cleanupDb } from '../utils/cleanup';
import { poll } from '../utils/poll';
import { createAndLoginUser, TEST_EXAMPLE_DOMAIN } from '../utils/testHelpers';
import { cacheService } from '../../services/cacheService.server';

/**
 * @vitest-environment node
 */

describe('Public API Routes Integration Tests', () => {
  // Shared state for tests
  let request: ReturnType<typeof supertest>;
  let testUser: UserProfile;
  let testRecipe: Recipe;
  let testFlyer: Flyer;
  let testStoreId: number;
  const createdRecipeCommentIds: number[] = [];

  beforeAll(async () => {
    vi.stubEnv('FRONTEND_URL', 'https://example.com');
    const app = (await import('../../../server')).default;
    request = supertest(app);

    const pool = getPool();
    // Create a user to own the recipe
    const userEmail = `public-routes-user-${Date.now()}@example.com`;
    // Use the helper to create a user and ensure a full UserProfile is returned,
    // which also handles activity logging correctly.
    const { user: createdUser } = await createAndLoginUser({
      email: userEmail,
      password: 'a-Very-Strong-Password-123!',
      fullName: 'Public Routes Test User',
      request,
    });
    testUser = createdUser;

    // Poll to ensure the user record has propagated before creating dependent records.
    await poll(
      () => pool.query('SELECT 1 FROM public.users WHERE user_id = $1', [testUser.user.user_id]),
      (result) => (result.rowCount ?? 0) > 0,
      { timeout: 5000, interval: 500, description: `user ${testUser.user.user_id} to persist` },
    );

    // Create a recipe
    const recipeRes = await pool.query(
      `INSERT INTO public.recipes (name, instructions, user_id, status) VALUES ('Public Test Recipe', 'Instructions here', $1, 'public') RETURNING *`,
      [testUser.user.user_id],
    );
    testRecipe = recipeRes.rows[0];

    // Create a store and flyer
    const storeRes = await pool.query(
      `INSERT INTO public.stores (name) VALUES ('Public Routes Test Store') RETURNING store_id`,
    );
    testStoreId = storeRes.rows[0].store_id;
    const flyerRes = await pool.query(
      `INSERT INTO public.flyers (store_id, file_name, image_url, icon_url, item_count, checksum) 
       VALUES ($1, 'public-routes-test.jpg', '${TEST_EXAMPLE_DOMAIN}/flyer-images/public-routes-test.jpg', '${TEST_EXAMPLE_DOMAIN}/flyer-images/icons/public-routes-test.jpg', 1, $2) RETURNING *`,
      [testStoreId, `${Date.now().toString(16)}`.padEnd(64, '0')],
    );
    testFlyer = flyerRes.rows[0];

    // Add an item to the flyer
    await pool.query(
      `INSERT INTO public.flyer_items (flyer_id, item, price_display, quantity) VALUES ($1, 'Test Item', '$0.00', 'each')`,
      [testFlyer.flyer_id],
    );

    // CRITICAL: Invalidate the flyer cache so the API sees the newly created flyer.
    // Without this, the cached response from previous tests/seed data won't include our test flyer.
    await cacheService.invalidateFlyers();
  });

  afterAll(async () => {
    vi.unstubAllEnvs();
    await cleanupDb({
      userIds: testUser ? [testUser.user.user_id] : [],
      recipeIds: testRecipe ? [testRecipe.recipe_id] : [],
      flyerIds: testFlyer ? [testFlyer.flyer_id] : [],
      storeIds: testStoreId ? [testStoreId] : [],
      recipeCommentIds: createdRecipeCommentIds,
    });
  });

  describe('Health Check Endpoints', () => {
    it('GET /api/health/ping should return "pong"', async () => {
      const response = await request.get('/api/health/ping');
      expect(response.status).toBe(200);
      expect(response.body.data.message).toBe('pong');
    });

    it('GET /api/health/db-schema should return success', async () => {
      const response = await request.get('/api/health/db-schema');
      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
    });

    it('GET /api/health/storage should return success', async () => {
      const response = await request.get('/api/health/storage');
      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
    });

    it('GET /api/health/db-pool should return success', async () => {
      const response = await request.get('/api/health/db-pool');
      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
    });

    it('GET /api/health/time should return the server time', async () => {
      const response = await request.get('/api/health/time');
      expect(response.status).toBe(200);
      expect(response.body.data).toHaveProperty('currentTime');
      expect(response.body.data).toHaveProperty('year');
      expect(response.body.data).toHaveProperty('week');
    });
  });

  describe('Public Data Endpoints', () => {
    it('GET /api/flyers should return a list of flyers', async () => {
      const response = await request.get('/api/flyers');
      const flyers: Flyer[] = response.body.data;
      expect(flyers.length).toBeGreaterThan(0);
      const foundFlyer = flyers.find((f) => f.flyer_id === testFlyer.flyer_id);
      expect(foundFlyer).toBeDefined();
      expect(foundFlyer).toHaveProperty('store');
    });

    it('GET /api/flyers/:id/items should return items for a specific flyer', async () => {
      const response = await request.get(`/api/flyers/${testFlyer.flyer_id}/items`);
      const items: FlyerItem[] = response.body.data;
      expect(response.status).toBe(200);
      expect(items).toBeInstanceOf(Array);
      expect(items.length).toBe(1);
      expect(items[0].flyer_id).toBe(testFlyer.flyer_id);
    });

    it('POST /api/flyers/items/batch-fetch should return items for multiple flyers', async () => {
      const flyerIds = [testFlyer.flyer_id];
      const response = await request.post('/api/flyers/items/batch-fetch').send({ flyerIds });
      const items: FlyerItem[] = response.body.data;
      expect(response.status).toBe(200);
      expect(items).toBeInstanceOf(Array);
      expect(items.length).toBeGreaterThan(0);
    });

    it('POST /api/flyers/items/batch-count should return a count for multiple flyers', async () => {
      const flyerIds = [testFlyer.flyer_id];
      const response = await request.post('/api/flyers/items/batch-count').send({ flyerIds });
      expect(response.status).toBe(200);
      expect(response.body.data.count).toBeTypeOf('number');
      expect(response.body.data.count).toBeGreaterThan(0);
    });

    it('GET /api/personalization/master-items should return a list of master grocery items', async () => {
      const response = await request.get('/api/personalization/master-items');
      expect(response.status).toBe(200);
      // The endpoint returns { items: [...], total: N } for pagination support
      expect(response.body.data).toHaveProperty('items');
      expect(response.body.data).toHaveProperty('total');
      const masterItems = response.body.data.items;
      expect(masterItems).toBeInstanceOf(Array);
      expect(masterItems.length).toBeGreaterThan(0); // This relies on seed data for master items.
      expect(masterItems[0]).toHaveProperty('master_grocery_item_id');
    });

    it('GET /api/recipes/by-sale-percentage should return recipes', async () => {
      const response = await request.get('/api/recipes/by-sale-percentage?minPercentage=10');
      const recipes: Recipe[] = response.body.data;
      expect(response.status).toBe(200);
      expect(recipes).toBeInstanceOf(Array);
    });

    it('GET /api/recipes/by-ingredient-and-tag should return recipes', async () => {
      // This test is now less brittle. It might return our created recipe or others.
      const response = await request.get(
        '/api/recipes/by-ingredient-and-tag?ingredient=Test&tag=Public',
      );
      const recipes: Recipe[] = response.body.data;
      expect(response.status).toBe(200);
      expect(recipes).toBeInstanceOf(Array);
    });

    it('GET /api/recipes/:recipeId/comments should return comments for a recipe', async () => {
      // Add a comment to our test recipe first
      const commentRes = await getPool().query(
        `INSERT INTO public.recipe_comments (recipe_id, user_id, content) VALUES ($1, $2, 'Test comment') RETURNING recipe_comment_id`,
        [testRecipe.recipe_id, testUser.user.user_id],
      );
      createdRecipeCommentIds.push(commentRes.rows[0].recipe_comment_id);
      const response = await request.get(`/api/recipes/${testRecipe.recipe_id}/comments`);
      const comments: RecipeComment[] = response.body.data;
      expect(response.status).toBe(200);
      expect(comments).toBeInstanceOf(Array);
      expect(comments.length).toBe(1);
      expect(comments[0].content).toBe('Test comment');
    });

    it('GET /api/stats/most-frequent-sales should return frequent items', async () => {
      const response = await request.get('/api/stats/most-frequent-sales?days=365&limit=5');
      const items = response.body.data;
      expect(response.status).toBe(200);
      expect(items).toBeInstanceOf(Array);
    });

    it('GET /api/personalization/dietary-restrictions should return a list of restrictions', async () => {
      // This test relies on static seed data for a lookup table, which is acceptable.
      const response = await request.get('/api/personalization/dietary-restrictions');
      const restrictions: DietaryRestriction[] = response.body.data;
      expect(response.status).toBe(200);
      expect(restrictions).toBeInstanceOf(Array);
      expect(restrictions.length).toBeGreaterThan(0);
      expect(restrictions[0]).toHaveProperty('dietary_restriction_id');
    });

    it('GET /api/personalization/appliances should return a list of appliances', async () => {
      const response = await request.get('/api/personalization/appliances');
      const appliances: Appliance[] = response.body.data;
      expect(response.status).toBe(200);
      expect(appliances).toBeInstanceOf(Array);
      expect(appliances.length).toBeGreaterThan(0);
      expect(appliances[0]).toHaveProperty('appliance_id');
    });
  });

  describe('Rate Limiting on Public Routes', () => {
    it('should block requests to /api/personalization/master-items after exceeding the limit', async () => {
      // The limit might be higher than 5. We loop enough times to ensure we hit the rate limit.
      const maxRequests = 120; // Increased to ensure we hit the limit (likely 60 or 100)
      let blockedResponse: any;

      for (let i = 0; i < maxRequests; i++) {
        const response = await request
          .get('/api/personalization/master-items')
          .set('X-Test-Rate-Limit-Enable', 'true'); // Enable rate limiter middleware

        if (response.status === 429) {
          blockedResponse = response;
          break;
        }
        expect(response.status).toBe(200);
      }

      expect(blockedResponse).toBeDefined();
      expect(blockedResponse.status).toBe(429);
    });
  });
});
