// src/routes/budget.routes.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import { Request, Response, NextFunction } from 'express';
import budgetRouter from './budget.routes';
import * as db from '../services/db/index.db';
import { createMockUserProfile, createMockBudget, createMockSpendingByCategory } from '../tests/utils/mockFactories';
import { mockLogger } from '../tests/utils/mockLogger';
import { createTestApp } from '../tests/utils/createTestApp';
import { ForeignKeyConstraintError, NotFoundError } from '../services/db/errors.db';
// 1. Mock the Service Layer directly.
// This decouples the route tests from the database logic.
vi.mock('../services/db/index.db', () => ({
  budgetRepo: {
    getBudgetsForUser: vi.fn(),
    createBudget: vi.fn(),
    updateBudget: vi.fn(),
    deleteBudget: vi.fn(),
    getSpendingByCategory: vi.fn(),
  },
  userRepo: {
    findUserProfileById: vi.fn(),
  },
}));

// Mock the logger to keep test output clean
vi.mock('../services/logger.server', () => ({
  logger: mockLogger,
}));

const mockUser = createMockUserProfile({
  user_id: 'user-123',
  user: { user_id: 'user-123', email: 'test@test.com' }
});

// Standardized mock for passport.routes
vi.mock('./passport.routes', () => ({
  default: {
    authenticate: vi.fn(() => (req: Request, res: Response, next: NextFunction) => {
      req.user = mockUser;
      next();
    }),
    initialize: () => (req: Request, res: Response, next: NextFunction) => next(),
  },
}));

// Define a reusable matcher for the logger object.
const expectLogger = expect.objectContaining({
  info: expect.any(Function),
  error: expect.any(Function),
});

describe('Budget Routes (/api/budgets)', () => {
  const mockUserProfile = createMockUserProfile({ user_id: 'user-123', points: 100 });

  beforeEach(() => {
    vi.clearAllMocks();
    // Provide default mock implementations to prevent undefined errors.
    // Individual tests can override these with more specific values.
    vi.mocked(db.budgetRepo.getBudgetsForUser).mockResolvedValue([]);
    vi.mocked(db.budgetRepo.getSpendingByCategory).mockResolvedValue([]);
  });
  
  const app = createTestApp({ router: budgetRouter, basePath: '/api/budgets', authenticatedUser: mockUser });

  // Add a basic error handler to capture errors passed to next(err) and return JSON.
  // This prevents unhandled error crashes in tests and ensures we get the 500 response we expect.
  app.use((err: any, req: any, res: any, next: any) => {
    res.status(err.status || 500).json({ message: err.message, errors: err.errors });
  });

    describe('GET /', () => {
      it('should return a list of budgets for the user', async () => {
        const mockBudgets = [createMockBudget({ budget_id: 1, user_id: 'user-123' })];
        // Mock the service function directly
        vi.mocked(db.budgetRepo.getBudgetsForUser).mockResolvedValue(mockBudgets);

        const response = await supertest(app).get('/api/budgets');

        expect(response.status).toBe(200);
        expect(response.body).toEqual(mockBudgets);
        expect(db.budgetRepo.getBudgetsForUser).toHaveBeenCalledWith(mockUserProfile.user_id, expectLogger);
      });

      it('should return 500 if the database call fails', async () => {
        vi.mocked(db.budgetRepo.getBudgetsForUser).mockRejectedValue(new Error('DB Error'));
        const response = await supertest(app).get('/api/budgets');
        expect(response.status).toBe(500); // The custom handler will now be used
        expect(response.body.message).toBe('DB Error');
      });
    });

    describe('POST /', () => {
      it('should create a new budget and return it', async () => {
        const newBudgetData = { name: 'Entertainment', amount_cents: 10000, period: 'monthly' as const, start_date: '2024-01-01' };
        const mockCreatedBudget = createMockBudget({ budget_id: 2, user_id: 'user-123', ...newBudgetData });
        // Mock the service function
        vi.mocked(db.budgetRepo.createBudget).mockResolvedValue(mockCreatedBudget);

        const response = await supertest(app).post('/api/budgets').send(newBudgetData);

        expect(response.status).toBe(201);
        expect(response.body).toEqual(mockCreatedBudget);
      });

      it('should return 400 if the user does not exist', async () => {
        const newBudgetData = { name: 'Entertainment', amount_cents: 10000, period: 'monthly' as const, start_date: '2024-01-01' };
        vi.mocked(db.budgetRepo.createBudget).mockRejectedValue(new ForeignKeyConstraintError('User not found'));
        const response = await supertest(app).post('/api/budgets').send(newBudgetData);
        expect(response.status).toBe(400);
        expect(response.body.message).toBe('User not found');
      });

      it('should return 500 if a generic database error occurs', async () => {
        const newBudgetData = { name: 'Entertainment', amount_cents: 10000, period: 'monthly' as const, start_date: '2024-01-01' };
        vi.mocked(db.budgetRepo.createBudget).mockRejectedValue(new Error('DB Error'));
        const response = await supertest(app).post('/api/budgets').send(newBudgetData);
        expect(response.status).toBe(500);
        expect(response.body.message).toBe('DB Error');
      });

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

        const response = await supertest(app).post('/api/budgets').send(invalidData);

        expect(response.status).toBe(400);
        expect(response.body.errors).toHaveLength(4);
      });
    });

    describe('PUT /:id', () => {
      it('should update an existing budget', async () => {
        const budgetUpdates = { amount_cents: 60000 };
        const mockUpdatedBudget = createMockBudget({ budget_id: 1, user_id: 'user-123', ...budgetUpdates });
        // Mock the service function
        vi.mocked(db.budgetRepo.updateBudget).mockResolvedValue(mockUpdatedBudget);

        const response = await supertest(app).put('/api/budgets/1').send(budgetUpdates);

        expect(response.status).toBe(200);
        expect(response.body).toEqual(mockUpdatedBudget);
      });

      it('should return 404 if the budget is not found', async () => {
        vi.mocked(db.budgetRepo.updateBudget).mockRejectedValue(new NotFoundError('Budget not found'));
        const response = await supertest(app).put('/api/budgets/999').send({ amount_cents: 1 });
        expect(response.status).toBe(404);
        expect(response.body.message).toBe('Budget not found');
      });

      it('should return 500 if a generic database error occurs', async () => {
        const budgetUpdates = { amount_cents: 60000 };
        vi.mocked(db.budgetRepo.updateBudget).mockRejectedValue(new Error('DB Error'));
        const response = await supertest(app).put('/api/budgets/1').send(budgetUpdates);
        expect(response.status).toBe(500); // The custom handler will now be used
        expect(response.body.message).toBe('DB Error');
      });

      it('should return 400 if no update fields are provided', async () => {
        const response = await supertest(app).put('/api/budgets/1').send({});
        expect(response.status).toBe(400);
        expect(response.body.errors[0].message).toBe('At least one field to update must be provided.');
      });
    });

    describe('DELETE /:id', () => {
      it('should delete a budget', async () => {
        // Mock the service function to resolve (void)
        vi.mocked(db.budgetRepo.deleteBudget).mockResolvedValue(undefined);

        const response = await supertest(app).delete('/api/budgets/1');

        expect(response.status).toBe(204);
        expect(db.budgetRepo.deleteBudget).toHaveBeenCalledWith(1, mockUserProfile.user_id, expectLogger);
      });

      it('should return 404 if the budget is not found', async () => {
        vi.mocked(db.budgetRepo.deleteBudget).mockRejectedValue(new NotFoundError('Budget not found'));
        const response = await supertest(app).delete('/api/budgets/999');
        expect(response.status).toBe(404);
        expect(response.body.message).toBe('Budget not found');
      });

      it('should return 500 if a generic database error occurs', async () => {
        vi.mocked(db.budgetRepo.deleteBudget).mockRejectedValue(new Error('DB Error'));
        const response = await supertest(app).delete('/api/budgets/1');
        expect(response.status).toBe(500); // The custom handler will now be used
        expect(response.body.message).toBe('DB Error');
      });
    });

    describe('GET /spending-analysis', () => {
      it('should return spending analysis data for a valid date range', async () => {
        const mockSpendingData = [createMockSpendingByCategory({ category_id: 1, category_name: 'Produce' })];
        // Mock the service function
        vi.mocked(db.budgetRepo.getSpendingByCategory).mockResolvedValue(mockSpendingData);

        const response = await supertest(app).get('/api/budgets/spending-analysis?startDate=2024-01-01&endDate=2024-01-31');

        expect(response.status).toBe(200);
        expect(response.body).toEqual(mockSpendingData);
      });

      it('should return 500 if the database call fails', async () => {
        // Mock the service function to throw
        vi.mocked(db.budgetRepo.getSpendingByCategory).mockRejectedValue(new Error('DB Error'));

        const response = await supertest(app).get('/api/budgets/spending-analysis?startDate=2024-01-01&endDate=2024-01-31');

        expect(response.status).toBe(500);
        expect(response.body.message).toBe('DB Error');
      });

      it('should return 400 for invalid date formats', async () => {
        const response = await supertest(app).get('/api/budgets/spending-analysis?startDate=2024/01/01&endDate=invalid');
        expect(response.status).toBe(400);
        expect(response.body.errors).toHaveLength(2);
      });
    });
});