// src/middleware/errorHandler.test.ts
import { describe, it, expect, vi, beforeEach, afterAll } from 'vitest';
import supertest from 'supertest';
import express, { Request, Response, NextFunction } from 'express';
import { errorHandler } from './errorHandler'; // This was a duplicate, fixed.
import { DatabaseError, ForeignKeyConstraintError, UniqueConstraintError, ValidationError } from '../services/db/errors.db';
import type { Logger } from 'pino';

// Create a mock logger that we can inject into requests and assert against.
// We only mock the methods we intend to spy on. The rest of the complex Pino
// Logger type is satisfied by casting, which is a common and clean testing practice.
const mockLogger = {
  error: vi.fn(),
  warn: vi.fn(),
  info: vi.fn(),
  debug: vi.fn(),
  fatal: vi.fn(),
  trace: vi.fn(),
  silent: vi.fn(),
  child: vi.fn().mockReturnThis(),
} as unknown as Logger;

// Mock the global logger as a fallback, though our tests will focus on req.log
vi.mock('../services/logger.server', () => ({ logger: mockLogger }));

// Mock console.error for testing NODE_ENV === 'test' behavior
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

// 1. Create a minimal Express app for testing
const app = express();
app.use(express.json());
// Add a middleware to inject our mock logger into each request as `req.log`
app.use((req: Request, res: Response, next: NextFunction) => {
  req.log = mockLogger;
  next();
});

// 2. Setup test routes that intentionally throw different kinds of errors
app.get('/generic-error', (req, res, next) => {
  next(new Error('A generic server error occurred.'));
});

app.get('/http-error-404', (req, res, next) => {
  // Create an error object with a custom status property for testing.
  const err = new Error('Resource not found') as Error & { status?: number };
  err.status = 404; // This is a common pattern in Express for non-custom errors.
  next(err);
});

app.get('/fk-error', (req, res, next) => {
  next(new ForeignKeyConstraintError('The referenced item does not exist.'));
});

app.get('/unique-error', (req, res, next) => {
  next(new UniqueConstraintError('This item already exists.'));
});

app.get('/db-error-500', (req, res, next) => {
  next(new DatabaseError('A database connection issue occurred.', 500));
});

app.get('/unauthorized-error-no-status', (req, res, next) => {
  const err = new Error('Invalid Token') as Error & { status?: number };
  err.name = 'UnauthorizedError'; // Simulate an error from a library like express-jwt
  next(err);
});

app.get('/unauthorized-error-with-status', (req, res, next) => {
  const err = new Error('Invalid Token') as Error & { status?: number };
  err.name = 'UnauthorizedError';
  err.status = 401; // Explicitly set status
  next(err);
});

app.get('/validation-error', (req, res, next) => {
  const validationIssues = [{ path: ['body', 'email'], message: 'Invalid email format' }];
  next(new ValidationError(validationIssues, 'Input validation failed'));
});

// 3. Apply the errorHandler middleware *after* all the routes
app.use(errorHandler);

describe('errorHandler Middleware', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    consoleErrorSpy.mockClear(); // Clear spy for console.error
    // Ensure NODE_ENV is set to 'test' for console.error logging
    process.env.NODE_ENV = 'test';
  });

  afterAll(() => {
    consoleErrorSpy.mockRestore(); // Restore console.error after all tests
    delete process.env.NODE_ENV; // Clean up environment variable
  });

  it('should return a generic 500 error for a standard Error object', async () => {
    const response = await supertest(app).get('/generic-error');
    expect(response.status).toBe(500);
    expect(response.body).toEqual({ message: 'A generic server error occurred.' });
    expect(mockLogger.error).toHaveBeenCalledWith(
      expect.objectContaining({
        err: expect.any(Error),
        errorId: expect.any(String),
        req: expect.objectContaining({ method: 'GET', url: '/generic-error' }),
      }),
      expect.stringMatching(/Unhandled API Error \(ID: \w+\)/)
    );
    expect(consoleErrorSpy).toHaveBeenCalledWith(
      expect.stringContaining('--- [TEST] UNHANDLED ERROR ---'),
      expect.any(Error)
    );
  });

  it('should use the status code and message from an Error object with a custom status (404)', async () => {
    const response = await supertest(app).get('/http-error-404');

    expect(response.status).toBe(404);
    expect(response.body).toEqual({ message: 'Resource not found' });
    expect(mockLogger.error).not.toHaveBeenCalled(); // 4xx errors are not logged as server errors
    expect(mockLogger.warn).toHaveBeenCalledWith(
      {
        err: expect.any(Error),
        validationErrors: undefined,
        statusCode: 404,
      },
      'Client Error on GET /http-error-404: Resource not found'
    );
    expect(consoleErrorSpy).toHaveBeenCalledWith(
      expect.stringContaining('--- [TEST] UNHANDLED ERROR ---'),
      expect.any(Error)
    );
  });

  it('should handle a ForeignKeyConstraintError with a 400 status and the specific error message', async () => {
    const response = await supertest(app).get('/fk-error');

    expect(response.status).toBe(400);
    expect(response.body).toEqual({ message: 'The referenced item does not exist.' });
    expect(mockLogger.error).not.toHaveBeenCalled();
    expect(mockLogger.warn).toHaveBeenCalledWith(
      {
        err: expect.any(ForeignKeyConstraintError),
        validationErrors: undefined,
        statusCode: 400,
      },
      'Client Error on GET /fk-error: The referenced item does not exist.'
    );
    expect(consoleErrorSpy).toHaveBeenCalledWith(
      expect.stringContaining('--- [TEST] UNHANDLED ERROR ---'),
      expect.any(ForeignKeyConstraintError)
    );
  });

  it('should handle a UniqueConstraintError with a 409 status and the specific error message', async () => {
    const response = await supertest(app).get('/unique-error');

    expect(response.status).toBe(409); // 409 Conflict
    expect(response.body).toEqual({ message: 'This item already exists.' });
    expect(mockLogger.error).not.toHaveBeenCalled();
    expect(mockLogger.warn).toHaveBeenCalledWith(
      {
        err: expect.any(UniqueConstraintError),
        validationErrors: undefined,
        statusCode: 409,
      },
      'Client Error on GET /unique-error: This item already exists.'
    );
    expect(consoleErrorSpy).toHaveBeenCalledWith(
      expect.stringContaining('--- [TEST] UNHANDLED ERROR ---'),
      expect.any(UniqueConstraintError)
    );
  });

  it('should handle a ValidationError with a 400 status and include the validation errors array', async () => {
    const response = await supertest(app).get('/validation-error');

    expect(response.status).toBe(400);
    expect(response.body.message).toBe('Input validation failed');
    expect(response.body.errors).toBeDefined();
    expect(response.body.errors).toEqual([{ path: ['body', 'email'], message: 'Invalid email format' }]);
    expect(mockLogger.error).not.toHaveBeenCalled(); // 4xx errors are not logged as server errors
    expect(mockLogger.warn).toHaveBeenCalledWith(
      {
        err: expect.any(ValidationError),
        validationErrors: [{ path: ['body', 'email'], message: 'Invalid email format' }],
        statusCode: 400,
      },
      'Client Error on GET /validation-error: Input validation failed'
    );
    expect(consoleErrorSpy).toHaveBeenCalledWith(
      expect.stringContaining('--- [TEST] UNHANDLED ERROR ---'),
      expect.any(ValidationError)
    );
  });

  it('should handle a DatabaseError with a 500 status and a generic message', async () => {
    const response = await supertest(app).get('/db-error-500');

    expect(response.status).toBe(500);
    expect(response.body).toEqual({ message: 'A database connection issue occurred.' });
    expect(mockLogger.error).toHaveBeenCalledWith(
      expect.objectContaining({
        err: expect.any(DatabaseError),
        errorId: expect.any(String),
        req: expect.objectContaining({ method: 'GET', url: '/db-error-500' }),
      }),
      expect.stringMatching(/Unhandled API Error \(ID: \w+\)/)
    );
    expect(consoleErrorSpy).toHaveBeenCalledWith(
      expect.stringContaining('--- [TEST] UNHANDLED ERROR ---'),
      expect.any(DatabaseError)
    );
  });

  it('should call next(err) if headers have already been sent', () => {
    // Supertest doesn't easily allow simulating res.headersSent = true mid-request
    // We need to mock the express response object directly for this specific test.
    const mockRequestDirect: Partial<Request> = { path: '/headers-sent-error', method: 'GET' };
    const mockResponseDirect: Partial<Response> = {
      status: vi.fn().mockReturnThis(),
      json: vi.fn(),
      headersSent: true, // Manually set headersSent to true
    };
    const mockNextDirect = vi.fn();
    const error = new Error('Error after headers sent');

    errorHandler(error, mockRequestDirect as Request, mockResponseDirect as Response, mockNextDirect);

    expect(mockNextDirect).toHaveBeenCalledWith(error);
    expect(mockResponseDirect.status).not.toHaveBeenCalled();
    expect(mockResponseDirect.json).not.toHaveBeenCalled();
    expect(mockLogger.error).not.toHaveBeenCalled(); // Should not log if delegated
    expect(consoleErrorSpy).not.toHaveBeenCalled(); // Should not log if delegated
  });

  describe('when NODE_ENV is "production"', () => {
    beforeEach(() => {
      process.env.NODE_ENV = 'production';
    });

    afterAll(() => {
      process.env.NODE_ENV = 'test'; // Reset for other test files
    });

    it('should return a generic message with an error ID for a 500 error', async () => {
      const response = await supertest(app).get('/generic-error');

      expect(response.status).toBe(500);
      expect(response.body.message).toMatch(/An unexpected server error occurred. Please reference error ID: \w+/);
    });
  });
});