// src/middleware/errorHandler.test.ts
import { describe, it, expect, vi, beforeEach, afterAll, afterEach } from 'vitest';
import supertest from 'supertest';
import express, { Request, Response, NextFunction } from 'express';
import { errorHandler } from './errorHandler'; // This was a duplicate, fixed.
import { DatabaseError } from '../services/processingErrors';
import {
  ForeignKeyConstraintError,
  UniqueConstraintError,
  ValidationError,
  NotFoundError,
} from '../services/db/errors.db';
import { createMockRequest } from '../tests/utils/createMockRequest';
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 } = vi.hoisted(() => {
  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(),
  };
  return { mockLogger };
});

// 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 as unknown as Logger;
  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('/not-found-error', (req, res, next) => {
  next(new NotFoundError('Specific resource missing'));
});

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.'));
});

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
    vi.stubEnv('NODE_ENV', 'test');
  });

  afterEach(() => {
    vi.unstubAllEnvs(); // Clean up environment variable stubs after each test
  });

  afterAll(() => {
    consoleErrorSpy.mockRestore(); // Restore console.error after all tests
  });

  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);
    // In test/dev, we now expect a stack trace for 5xx errors.
    expect(response.body.error.message).toBe('A generic server error occurred.');
    expect(response.body.error.details.stack).toBeDefined();
    expect(response.body.meta.requestId).toEqual(expect.any(String));
    console.log(
      '[DEBUG] errorHandler.test.ts: Received 500 error response with ID:',
      response.body.meta.requestId,
    );
    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.stringMatching(/--- \[TEST\] UNHANDLED ERROR \(ID: \w+\) ---/),
      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({
      success: false,
      error: { code: 'NOT_FOUND', 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),
        statusCode: 404,
      },
      'Client Error on GET /http-error-404: Resource not found',
    );
    expect(consoleErrorSpy).not.toHaveBeenCalled();
  });

  it('should handle a NotFoundError with a 404 status', async () => {
    const response = await supertest(app).get('/not-found-error');

    expect(response.status).toBe(404);
    expect(response.body).toEqual({
      success: false,
      error: { code: 'NOT_FOUND', message: 'Specific resource missing' },
    });
    expect(mockLogger.error).not.toHaveBeenCalled();
    expect(mockLogger.warn).toHaveBeenCalledWith(
      {
        err: expect.any(NotFoundError),
        statusCode: 404,
      },
      'Client Error on GET /not-found-error: Specific resource missing',
    );
    expect(consoleErrorSpy).not.toHaveBeenCalled();
  });

  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({
      success: false,
      error: { code: 'BAD_REQUEST', message: 'The referenced item does not exist.' },
    });
    expect(mockLogger.error).not.toHaveBeenCalled();
    expect(mockLogger.warn).toHaveBeenCalledWith(
      {
        err: expect.any(ForeignKeyConstraintError),
        statusCode: 400,
      },
      'Client Error on GET /fk-error: The referenced item does not exist.',
    );
    expect(consoleErrorSpy).not.toHaveBeenCalled();
  });

  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({
      success: false,
      error: { code: 'CONFLICT', message: 'This item already exists.' },
    });
    expect(mockLogger.error).not.toHaveBeenCalled();
    expect(mockLogger.warn).toHaveBeenCalledWith(
      {
        err: expect.any(UniqueConstraintError),
        statusCode: 409,
      },
      'Client Error on GET /unique-error: This item already exists.',
    );
    expect(consoleErrorSpy).not.toHaveBeenCalled();
  });

  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.error.message).toBe('Input validation failed');
    expect(response.body.error.details).toBeDefined();
    expect(response.body.error.details).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).not.toHaveBeenCalled();
  });

  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);
    // In test/dev, we now expect a stack trace for 5xx errors.
    expect(response.body.error.message).toBe('A database connection issue occurred.');
    expect(response.body.error.details.stack).toBeDefined();
    expect(response.body.meta.requestId).toEqual(expect.any(String));
    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.stringMatching(/--- \[TEST\] UNHANDLED ERROR \(ID: \w+\) ---/),
      expect.any(DatabaseError),
    );
  });

  it('should handle an UnauthorizedError with default 401 status', async () => {
    const response = await supertest(app).get('/unauthorized-error-no-status');

    expect(response.status).toBe(401);
    expect(response.body).toEqual({
      success: false,
      error: { code: 'UNAUTHORIZED', message: 'Invalid Token' },
    });
    expect(mockLogger.warn).toHaveBeenCalledWith(
      {
        err: expect.any(Error),
        statusCode: 401,
      },
      'Client Error on GET /unauthorized-error-no-status: Invalid Token',
    );
    expect(consoleErrorSpy).not.toHaveBeenCalled();
  });

  it('should handle an UnauthorizedError with explicit status', async () => {
    const response = await supertest(app).get('/unauthorized-error-with-status');

    expect(response.status).toBe(401);
    expect(response.body).toEqual({
      success: false,
      error: { code: 'UNAUTHORIZED', message: 'Invalid Token' },
    });
    expect(mockLogger.warn).toHaveBeenCalledWith(
      {
        err: expect.any(Error),
        statusCode: 401,
      },
      'Client Error on GET /unauthorized-error-with-status: Invalid Token',
    );
    expect(consoleErrorSpy).not.toHaveBeenCalled();
  });

  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 = createMockRequest({ 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(() => {
      vi.stubEnv('NODE_ENV', 'production');
    });

    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.error.message).toMatch(
        /An unexpected server error occurred. Please reference error ID: \w+/,
      );
      expect(response.body.error.details?.stack).toBeUndefined();
    });

    it('should return the actual error message for client errors (4xx) in production', async () => {
      const response = await supertest(app).get('/http-error-404');

      expect(response.status).toBe(404);
      expect(response.body.error.message).toBe('Resource not found');
    });
  });
});
