// src/tests/integration/recipe.integration.test.ts
import { describe, it, expect, beforeAll, afterAll, vi, afterEach } from 'vitest';
import supertest from 'supertest';
import { createAndLoginUser } from '../utils/testHelpers';
import { cleanupDb } from '../utils/cleanup';
import type { UserProfile, Recipe, RecipeComment } from '../../types';
import { getPool } from '../../services/db/connection.db';

import { aiService } from '../../services/aiService.server';

/**
 * @vitest-environment node
 */

describe('Recipe API Routes Integration Tests', () => {
  let request: ReturnType<typeof supertest>;
  let testUser: UserProfile;
  let authToken: string;
  let testRecipe: Recipe;
  const createdUserIds: string[] = [];
  const createdRecipeIds: number[] = [];

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

    // Create a user to own the recipe and perform authenticated actions
    const { user, token } = await createAndLoginUser({
      email: `recipe-user-${Date.now()}@example.com`,
      fullName: 'Recipe Test User',
      request,
    });
    testUser = user;
    authToken = token;
    createdUserIds.push(user.user.user_id);

    // Mock the AI service method using spyOn to preserve other exports like DuplicateFlyerError
    vi.spyOn(aiService, 'generateRecipeSuggestion').mockResolvedValue('Default Mock Suggestion');

    // Create a recipe owned by the test user
    const recipeRes = await getPool().query(
      `INSERT INTO public.recipes (name, instructions, user_id, status, description) 
       VALUES ('Integration Test Recipe', '1. Do this. 2. Do that.', $1, 'public', 'A test recipe description.') 
       RETURNING *`,
      [testUser.user.user_id],
    );
    testRecipe = recipeRes.rows[0];
    createdRecipeIds.push(testRecipe.recipe_id);
  });

  afterEach(() => {
    vi.clearAllMocks();
    // Reset the mock to its default state for the next test
    vi.mocked(aiService.generateRecipeSuggestion).mockResolvedValue('Default Mock Suggestion');
  });

  afterAll(async () => {
    vi.unstubAllEnvs();
    // Clean up all created resources
    await cleanupDb({
      userIds: createdUserIds,
      recipeIds: createdRecipeIds,
    });
  });

  describe('GET /api/recipes/:recipeId', () => {
    it('should fetch a single public recipe by its ID', async () => {
      const response = await request.get(`/api/recipes/${testRecipe.recipe_id}`);

      expect(response.status).toBe(200);
      expect(response.body.data).toBeDefined();
      expect(response.body.data.recipe_id).toBe(testRecipe.recipe_id);
      expect(response.body.data.name).toBe('Integration Test Recipe');
    });

    it('should return 404 for a non-existent recipe ID', async () => {
      const response = await request.get('/api/recipes/999999');
      expect(response.status).toBe(404);
    });
  });

  it('should allow an authenticated user to create a new recipe', async () => {
    const newRecipeData = {
      name: 'My New Awesome Recipe',
      instructions: '1. Be awesome. 2. Make recipe.',
      description: 'A recipe created during an integration test.',
    };

    const response = await request
      .post('/api/users/recipes')
      .set('Authorization', `Bearer ${authToken}`)
      .send(newRecipeData);

    // Assert the response from the POST request
    expect(response.status).toBe(201);
    const createdRecipe: Recipe = response.body.data;
    expect(createdRecipe).toBeDefined();
    expect(createdRecipe.recipe_id).toBeTypeOf('number');
    expect(createdRecipe.name).toBe(newRecipeData.name);
    expect(createdRecipe.user_id).toBe(testUser.user.user_id);

    // Add the new recipe ID to the cleanup array to ensure it's deleted after tests
    createdRecipeIds.push(createdRecipe.recipe_id);

    // Verify the recipe can be fetched from the public endpoint
    const verifyResponse = await request.get(`/api/recipes/${createdRecipe.recipe_id}`);
    expect(verifyResponse.status).toBe(200);
    expect(verifyResponse.body.data.name).toBe(newRecipeData.name);
  });
  it('should allow an authenticated user to update their own recipe', async () => {
    const recipeUpdates = {
      name: 'Updated Integration Test Recipe',
      instructions: '1. Do the new thing. 2. Do the other new thing.',
    };

    const response = await request
      .put(`/api/users/recipes/${testRecipe.recipe_id}`) // Authenticated recipe update endpoint
      .set('Authorization', `Bearer ${authToken}`)
      .send(recipeUpdates);

    // Assert the response from the PUT request
    expect(response.status).toBe(200);
    const updatedRecipe: Recipe = response.body.data;
    expect(updatedRecipe.name).toBe(recipeUpdates.name);
    expect(updatedRecipe.instructions).toBe(recipeUpdates.instructions);

    // Verify the changes were persisted by fetching the recipe again
    const verifyResponse = await request.get(`/api/recipes/${testRecipe.recipe_id}`);
    expect(verifyResponse.status).toBe(200);
    expect(verifyResponse.body.data.name).toBe(recipeUpdates.name);
  });
  it("should prevent a user from updating another user's recipe", async () => {
    // Create a second user who will try to update the first user's recipe
    const { user: otherUser, token: otherToken } = await createAndLoginUser({
      email: `recipe-other-${Date.now()}@example.com`,
      fullName: 'Other Recipe User',
      request,
    });
    createdUserIds.push(otherUser.user.user_id);

    // Attempt to update the testRecipe (owned by testUser) using otherUser's token
    const response = await request
      .put(`/api/users/recipes/${testRecipe.recipe_id}`)
      .set('Authorization', `Bearer ${otherToken}`)
      .send({ name: 'Hacked Recipe Name' });

    // Should return 404 because the recipe doesn't belong to this user
    expect(response.status).toBe(404);
  });

  it('should allow an authenticated user to delete their own recipe', async () => {
    // Create a recipe specifically for deletion
    const createRes = await request
      .post('/api/users/recipes')
      .set('Authorization', `Bearer ${authToken}`)
      .send({
        name: 'Recipe To Delete',
        instructions: 'This recipe will be deleted.',
        description: 'A temporary recipe.',
      });

    expect(createRes.status).toBe(201);
    const recipeToDelete: Recipe = createRes.body.data;

    // Delete the recipe
    const deleteRes = await request
      .delete(`/api/users/recipes/${recipeToDelete.recipe_id}`)
      .set('Authorization', `Bearer ${authToken}`);

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

    // Verify it's actually deleted by trying to fetch it
    const verifyRes = await request.get(`/api/recipes/${recipeToDelete.recipe_id}`);
    expect(verifyRes.status).toBe(404);
  });

  it("should prevent a user from deleting another user's recipe", async () => {
    // Create a second user who will try to delete the first user's recipe
    const { user: otherUser, token: otherToken } = await createAndLoginUser({
      email: `recipe-deleter-${Date.now()}@example.com`,
      fullName: 'Deleter User',
      request,
    });
    createdUserIds.push(otherUser.user.user_id);

    // Attempt to delete the testRecipe (owned by testUser) using otherUser's token
    const response = await request
      .delete(`/api/users/recipes/${testRecipe.recipe_id}`)
      .set('Authorization', `Bearer ${otherToken}`);

    // Should return 404 because the recipe doesn't belong to this user
    expect(response.status).toBe(404);

    // Verify the recipe still exists
    const verifyRes = await request.get(`/api/recipes/${testRecipe.recipe_id}`);
    expect(verifyRes.status).toBe(200);
  });

  it('should allow an authenticated user to post a comment on a recipe', async () => {
    const commentContent = 'This is a great recipe! Thanks for sharing.';

    const response = await request
      .post(`/api/recipes/${testRecipe.recipe_id}/comments`)
      .set('Authorization', `Bearer ${authToken}`)
      .send({ content: commentContent });

    expect(response.status).toBe(201);
    const comment: RecipeComment = response.body.data;
    expect(comment.content).toBe(commentContent);
    expect(comment.recipe_id).toBe(testRecipe.recipe_id);
    expect(comment.user_id).toBe(testUser.user.user_id);
    expect(comment.recipe_comment_id).toBeDefined();
  });

  it('should allow an authenticated user to fork a recipe', async () => {
    const response = await request
      .post(`/api/recipes/${testRecipe.recipe_id}/fork`)
      .set('Authorization', `Bearer ${authToken}`);

    expect(response.status).toBe(201);
    const forkedRecipe: Recipe = response.body.data;

    // The forked recipe should have a new ID but reference the original
    expect(forkedRecipe.recipe_id).not.toBe(testRecipe.recipe_id);
    expect(forkedRecipe.original_recipe_id).toBe(testRecipe.recipe_id);
    expect(forkedRecipe.user_id).toBe(testUser.user.user_id);
    // The name should include "(Fork)" suffix
    expect(forkedRecipe.name).toContain('Fork');

    // Track for cleanup
    createdRecipeIds.push(forkedRecipe.recipe_id);
  });

  describe('POST /api/recipes/suggest', () => {
    it('should return a recipe suggestion based on ingredients', async () => {
      const ingredients = ['chicken', 'rice', 'broccoli'];
      const mockSuggestion = 'Chicken and Broccoli Stir-fry with Rice';
      vi.mocked(aiService.generateRecipeSuggestion).mockResolvedValue(mockSuggestion);

      const response = await request
        .post('/api/recipes/suggest')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ ingredients });

      expect(response.status).toBe(200);
      expect(response.body.data).toEqual({ suggestion: mockSuggestion });
      expect(aiService.generateRecipeSuggestion).toHaveBeenCalledWith(
        ingredients,
        expect.anything(),
      );
    });
  });
});
