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

/**
 * @vitest-environment node
 */

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

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

    // 1. Create a user for the tests
    const { user, token } = await createAndLoginUser({
      email: `budget-user-${Date.now()}@example.com`,
      fullName: 'Budget Test User',
      request,
    });
    testUser = user;
    authToken = token;
    createdUserIds.push(user.user.user_id);

    // 2. Seed some budget data for this user directly in the DB for predictable testing
    const budgetToCreate = {
      name: 'Monthly Groceries',
      amount_cents: 50000, // $500.00
      period: 'monthly',
      start_date: '2025-01-01',
    };

    const budgetRes = await getPool().query(
      `INSERT INTO public.budgets (user_id, name, amount_cents, period, start_date) 
       VALUES ($1, $2, $3, $4, $5) 
       RETURNING *`,
      [
        testUser.user.user_id,
        budgetToCreate.name,
        budgetToCreate.amount_cents,
        budgetToCreate.period,
        budgetToCreate.start_date,
      ],
    );
    testBudget = budgetRes.rows[0];
    createdBudgetIds.push(testBudget.budget_id);
  });

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

  describe('GET /api/budgets', () => {
    it('should fetch budgets for the authenticated user', async () => {
      const response = await request
        .get('/api/budgets')
        .set('Authorization', `Bearer ${authToken}`);

      expect(response.status).toBe(200);
      const budgets: Budget[] = response.body.data;
      expect(budgets).toBeInstanceOf(Array);
      expect(budgets.some((b) => b.budget_id === testBudget.budget_id)).toBe(true);
    });

    it('should return 401 if user is not authenticated', async () => {
      const response = await request.get('/api/budgets');
      expect(response.status).toBe(401);
    });
  });

  describe('POST /api/budgets', () => {
    it('should allow an authenticated user to create a new budget', async () => {
      const newBudgetData = {
        name: 'Weekly Snacks',
        amount_cents: 15000, // $150.00
        period: 'weekly',
        start_date: '2025-02-01',
      };

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

      expect(response.status).toBe(201);
      const createdBudget: Budget = response.body.data;
      expect(createdBudget.name).toBe(newBudgetData.name);
      expect(createdBudget.amount_cents).toBe(newBudgetData.amount_cents);
      expect(createdBudget.period).toBe(newBudgetData.period);
      // The API returns a DATE column as ISO timestamp. Due to timezone differences,
      // the date might shift by a day. We verify the date is within 1 day of expected.
      const returnedDate = new Date(createdBudget.start_date);
      const expectedDate = new Date(newBudgetData.start_date + 'T12:00:00Z'); // Use noon UTC to avoid day shifts
      const daysDiff =
        Math.abs(returnedDate.getTime() - expectedDate.getTime()) / (1000 * 60 * 60 * 24);
      expect(daysDiff).toBeLessThanOrEqual(1);
      expect(createdBudget.user_id).toBe(testUser.user.user_id);
      expect(createdBudget.budget_id).toBeDefined();

      // Track for cleanup
      createdBudgetIds.push(createdBudget.budget_id);
    });

    it('should return 400 for invalid budget data', async () => {
      const invalidBudgetData = {
        name: '', // Invalid: empty name
        amount_cents: -100, // Invalid: negative amount
        period: 'daily', // Invalid: not 'weekly' or 'monthly'
        start_date: 'not-a-date',
      };

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

      expect(response.status).toBe(400);
    });

    it('should return 401 if user is not authenticated', async () => {
      const response = await request.post('/api/budgets').send({
        name: 'Unauthorized Budget',
        amount_cents: 10000,
        period: 'monthly',
        start_date: '2025-01-01',
      });

      expect(response.status).toBe(401);
    });
  });

  describe('PUT /api/budgets/:id', () => {
    it('should allow an authenticated user to update their own budget', async () => {
      const updatedData = {
        name: 'Updated Monthly Groceries',
        amount_cents: 60000, // $600.00
      };

      const response = await request
        .put(`/api/budgets/${testBudget.budget_id}`)
        .set('Authorization', `Bearer ${authToken}`)
        .send(updatedData);

      expect(response.status).toBe(200);
      const updatedBudget: Budget = response.body.data;
      expect(updatedBudget.name).toBe(updatedData.name);
      expect(updatedBudget.amount_cents).toBe(updatedData.amount_cents);
      // Unchanged fields should remain the same
      expect(updatedBudget.period).toBe(testBudget.period);
      // The seeded budget start_date is a plain DATE, but API may return ISO timestamp.
      // Due to timezone differences, verify the date is within 1 day of expected.
      const returnedDate = new Date(updatedBudget.start_date);
      const expectedDate = new Date('2025-01-01T12:00:00Z'); // Use noon UTC to avoid day shifts
      const daysDiff =
        Math.abs(returnedDate.getTime() - expectedDate.getTime()) / (1000 * 60 * 60 * 24);
      expect(daysDiff).toBeLessThanOrEqual(1);
    });

    it('should return 404 when updating a non-existent budget', async () => {
      const response = await request
        .put('/api/budgets/999999')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ name: 'Non-existent' });

      expect(response.status).toBe(404);
    });

    it('should return 400 when no update fields are provided', async () => {
      const response = await request
        .put(`/api/budgets/${testBudget.budget_id}`)
        .set('Authorization', `Bearer ${authToken}`)
        .send({});

      expect(response.status).toBe(400);
    });

    it('should return 401 if user is not authenticated', async () => {
      const response = await request
        .put(`/api/budgets/${testBudget.budget_id}`)
        .send({ name: 'Hacked Budget' });

      expect(response.status).toBe(401);
    });
  });

  describe('DELETE /api/budgets/:id', () => {
    it('should allow an authenticated user to delete their own budget', async () => {
      // Create a budget specifically for deletion
      const budgetToDelete = {
        name: 'To Be Deleted',
        amount_cents: 5000,
        period: 'weekly',
        start_date: '2025-03-01',
      };

      const createResponse = await request
        .post('/api/budgets')
        .set('Authorization', `Bearer ${authToken}`)
        .send(budgetToDelete);

      expect(createResponse.status).toBe(201);
      const createdBudget: Budget = createResponse.body.data;

      // Now delete it
      const deleteResponse = await request
        .delete(`/api/budgets/${createdBudget.budget_id}`)
        .set('Authorization', `Bearer ${authToken}`);

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

      // Verify it's actually deleted
      const getResponse = await request
        .get('/api/budgets')
        .set('Authorization', `Bearer ${authToken}`);

      const budgets: Budget[] = getResponse.body.data;
      expect(budgets.some((b) => b.budget_id === createdBudget.budget_id)).toBe(false);
    });

    it('should return 404 when deleting a non-existent budget', async () => {
      const response = await request
        .delete('/api/budgets/999999')
        .set('Authorization', `Bearer ${authToken}`);

      expect(response.status).toBe(404);
    });

    it('should return 401 if user is not authenticated', async () => {
      const response = await request.delete(`/api/budgets/${testBudget.budget_id}`);

      expect(response.status).toBe(401);
    });
  });

  describe('GET /api/budgets/spending-analysis', () => {
    it('should return spending analysis for the authenticated user', async () => {
      // Note: This test verifies the endpoint works and returns the correct structure.
      // In a real scenario with seeded shopping trip data, we'd verify actual values.
      const response = await request
        .get('/api/budgets/spending-analysis')
        .query({ startDate: '2025-01-01', endDate: '2025-12-31' })
        .set('Authorization', `Bearer ${authToken}`);

      expect(response.status).toBe(200);
      expect(response.body.data).toBeInstanceOf(Array);

      // Each item in the array should have the SpendingByCategory structure
      if (response.body.data.length > 0) {
        const firstItem = response.body.data[0];
        expect(firstItem).toHaveProperty('category_id');
        expect(firstItem).toHaveProperty('category_name');
        expect(firstItem).toHaveProperty('total_spent_cents');
      }
    });

    it('should return 400 for invalid date format', async () => {
      const response = await request
        .get('/api/budgets/spending-analysis')
        .query({ startDate: 'invalid-date', endDate: '2025-12-31' })
        .set('Authorization', `Bearer ${authToken}`);

      expect(response.status).toBe(400);
    });

    it('should return 400 when required query params are missing', async () => {
      const response = await request
        .get('/api/budgets/spending-analysis')
        .set('Authorization', `Bearer ${authToken}`);

      expect(response.status).toBe(400);
    });

    it('should return 401 if user is not authenticated', async () => {
      const response = await request
        .get('/api/budgets/spending-analysis')
        .query({ startDate: '2025-01-01', endDate: '2025-12-31' });

      expect(response.status).toBe(401);
    });
  });
});
