// src/services/db/errors.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { Logger } from 'pino';
import {
  RepositoryError,
  UniqueConstraintError,
  ForeignKeyConstraintError,
  NotFoundError,
  ForbiddenError,
  ValidationError,
  FileUploadError,
  NotNullConstraintError,
  CheckConstraintError,
  InvalidTextRepresentationError,
  NumericValueOutOfRangeError,
  handleDbError,
} from './errors.db';

vi.mock('./logger.server');

describe('Custom Database and Application Errors', () => {
  describe('RepositoryError', () => {
    it('should create a generic database error with a message and status', () => {
      const message = 'Generic DB Error';
      const status = 500;
      const error = new RepositoryError(message, status);

      expect(error).toBeInstanceOf(Error);
      expect(error).toBeInstanceOf(RepositoryError);
      expect(error.message).toBe(message);
      expect(error.status).toBe(status);
      expect(error.name).toBe('RepositoryError');
    });
  });

  describe('UniqueConstraintError', () => {
    it('should create an error with a default message and status 409', () => {
      const error = new UniqueConstraintError();

      expect(error).toBeInstanceOf(Error);
      expect(error).toBeInstanceOf(RepositoryError);
      expect(error).toBeInstanceOf(UniqueConstraintError);
      expect(error.message).toBe('The record already exists.');
      expect(error.status).toBe(409);
      expect(error.name).toBe('UniqueConstraintError');
    });

    it('should create an error with a custom message', () => {
      const message = 'This email is already taken.';
      const error = new UniqueConstraintError(message);
      expect(error.message).toBe(message);
    });
  });

  describe('ForeignKeyConstraintError', () => {
    it('should create an error with a default message and status 400', () => {
      const error = new ForeignKeyConstraintError();

      expect(error).toBeInstanceOf(Error);
      expect(error).toBeInstanceOf(RepositoryError);
      expect(error).toBeInstanceOf(ForeignKeyConstraintError);
      expect(error.message).toBe('The referenced record does not exist.');
      expect(error.status).toBe(400);
      expect(error.name).toBe('ForeignKeyConstraintError');
    });

    it('should create an error with a custom message', () => {
      const message = 'The specified user does not exist.';
      const error = new ForeignKeyConstraintError(message);
      expect(error.message).toBe(message);
    });
  });

  describe('NotFoundError', () => {
    it('should create an error with a default message and status 404', () => {
      const error = new NotFoundError();

      expect(error).toBeInstanceOf(Error);
      expect(error).toBeInstanceOf(RepositoryError);
      expect(error).toBeInstanceOf(NotFoundError);
      expect(error.message).toBe('The requested resource was not found.');
      expect(error.status).toBe(404);
      expect(error.name).toBe('NotFoundError');
    });

    it('should create an error with a custom message', () => {
      const message = 'Flyer with ID 999 not found.';
      const error = new NotFoundError(message);
      expect(error.message).toBe(message);
    });
  });

  describe('ForbiddenError', () => {
    it('should create an error with a default message and status 403', () => {
      const error = new ForbiddenError();

      expect(error).toBeInstanceOf(Error);
      expect(error).toBeInstanceOf(RepositoryError);
      expect(error).toBeInstanceOf(ForbiddenError);
      expect(error.message).toBe('Access denied.');
      expect(error.status).toBe(403);
      expect(error.name).toBe('ForbiddenError');
    });

    it('should create an error with a custom message', () => {
      const message = 'You shall not pass.';
      const error = new ForbiddenError(message);
      expect(error.message).toBe(message);
    });
  });

  describe('ValidationError', () => {
    it('should create an error with a default message, status 400, and validation errors array', () => {
      const validationIssues = [{ path: ['email'], message: 'Invalid email' }];
      const error = new ValidationError(validationIssues);

      expect(error).toBeInstanceOf(Error);
      expect(error).toBeInstanceOf(RepositoryError);
      expect(error).toBeInstanceOf(ValidationError);
      expect(error.message).toBe('The request data is invalid.');
      expect(error.status).toBe(400);
      expect(error.name).toBe('ValidationError');
      expect(error.validationErrors).toEqual(validationIssues);
    });

    it('should create an error with a custom message', () => {
      const message = 'Your input has some issues.';
      const error = new ValidationError([], message);
      expect(error.message).toBe(message);
    });
  });

  describe('FileUploadError', () => {
    it('should create an error with the correct message, name, and status 400', () => {
      const message = 'No file was uploaded.';
      const error = new FileUploadError(message);

      expect(error).toBeInstanceOf(Error);
      expect(error).toBeInstanceOf(FileUploadError);
      expect(error.message).toBe(message);
      expect(error.status).toBe(400);
      expect(error.name).toBe('FileUploadError');
    });
  });

  describe('NotNullConstraintError', () => {
    it('should create an error with a default message and status 400', () => {
      const error = new NotNullConstraintError();
      expect(error).toBeInstanceOf(RepositoryError);
      expect(error.message).toBe('A required field was left null.');
      expect(error.status).toBe(400);
      expect(error.name).toBe('NotNullConstraintError');
    });

    it('should create an error with a custom message', () => {
      const message = 'Email cannot be null.';
      const error = new NotNullConstraintError(message);
      expect(error.message).toBe(message);
    });
  });

  describe('CheckConstraintError', () => {
    it('should create an error with a default message and status 400', () => {
      const error = new CheckConstraintError();
      expect(error).toBeInstanceOf(RepositoryError);
      expect(error.message).toBe('A check constraint was violated.');
      expect(error.status).toBe(400);
      expect(error.name).toBe('CheckConstraintError');
    });

    it('should create an error with a custom message', () => {
      const message = 'Price must be positive.';
      const error = new CheckConstraintError(message);
      expect(error.message).toBe(message);
    });
  });

  describe('InvalidTextRepresentationError', () => {
    it('should create an error with a default message and status 400', () => {
      const error = new InvalidTextRepresentationError();
      expect(error).toBeInstanceOf(RepositoryError);
      expect(error.message).toBe('A value has an invalid format for its data type.');
      expect(error.status).toBe(400);
      expect(error.name).toBe('InvalidTextRepresentationError');
    });

    it('should create an error with a custom message', () => {
      const message = 'Invalid input syntax for type integer: "abc"';
      const error = new InvalidTextRepresentationError(message);
      expect(error.message).toBe(message);
    });
  });

  describe('NumericValueOutOfRangeError', () => {
    it('should create an error with a default message and status 400', () => {
      const error = new NumericValueOutOfRangeError();
      expect(error).toBeInstanceOf(RepositoryError);
      expect(error.message).toBe('A numeric value is out of the allowed range.');
      expect(error.status).toBe(400);
      expect(error.name).toBe('NumericValueOutOfRangeError');
    });

    it('should create an error with a custom message', () => {
      const message = 'Value too large for type smallint.';
      const error = new NumericValueOutOfRangeError(message);
      expect(error.message).toBe(message);
    });
  });

  describe('handleDbError', () => {
    const mockLogger = {
      error: vi.fn(),
    } as unknown as Logger;

    beforeEach(() => {
      vi.clearAllMocks();
    });

    it('should re-throw existing RepositoryError instances without logging', () => {
      const notFound = new NotFoundError('Test not found');
      expect(() => handleDbError(notFound, mockLogger, 'msg', {})).toThrow(notFound);
      expect(mockLogger.error).not.toHaveBeenCalled();
    });

    it('should throw UniqueConstraintError for code 23505', () => {
      const dbError = new Error('duplicate key');
      (dbError as any).code = '23505';
      expect(() =>
        handleDbError(dbError, mockLogger, 'msg', {}, { uniqueMessage: 'custom unique' }),
      ).toThrow('custom unique');
    });

    it('should throw ForeignKeyConstraintError for code 23503', () => {
      const dbError = new Error('fk violation');
      (dbError as any).code = '23503';
      expect(() =>
        handleDbError(dbError, mockLogger, 'msg', {}, { fkMessage: 'custom fk' }),
      ).toThrow('custom fk');
    });

    it('should throw NotNullConstraintError for code 23502', () => {
      const dbError = new Error('not null violation');
      (dbError as any).code = '23502';
      expect(() =>
        handleDbError(dbError, mockLogger, 'msg', {}, { notNullMessage: 'custom not null' }),
      ).toThrow('custom not null');
    });

    it('should throw CheckConstraintError for code 23514', () => {
      const dbError = new Error('check violation');
      (dbError as any).code = '23514';
      expect(() =>
        handleDbError(dbError, mockLogger, 'msg', {}, { checkMessage: 'custom check' }),
      ).toThrow('custom check');
    });

    it('should throw InvalidTextRepresentationError for code 22P02', () => {
      const dbError = new Error('invalid text');
      (dbError as any).code = '22P02';
      expect(() =>
        handleDbError(
          dbError,
          mockLogger,
          'msg',
          {},
          { invalidTextMessage: 'custom invalid text' },
        ),
      ).toThrow('custom invalid text');
    });

    it('should throw NumericValueOutOfRangeError for code 22003', () => {
      const dbError = new Error('out of range');
      (dbError as any).code = '22003';
      expect(() =>
        handleDbError(
          dbError,
          mockLogger,
          'msg',
          {},
          { numericOutOfRangeMessage: 'custom out of range' },
        ),
      ).toThrow('custom out of range');
    });

    it('should throw a generic Error with a default message', () => {
      const genericError = new Error('Something else happened');
      expect(() =>
        handleDbError(genericError, mockLogger, 'msg', {}, { defaultMessage: 'Oops' }),
      ).toThrow('Oops');
      expect(mockLogger.error).toHaveBeenCalledWith({ err: genericError }, 'msg');
    });

    it('should throw a generic Error with a constructed message using entityName', () => {
      const genericError = new Error('Something else happened');
      expect(() =>
        handleDbError(genericError, mockLogger, 'msg', {}, { entityName: 'User' }),
      ).toThrow('Failed to perform operation on User.');
    });

    it('should throw a generic Error with a constructed message using "database" as a fallback', () => {
      const genericError = new Error('Something else happened');
      // No defaultMessage or entityName provided
      expect(() => handleDbError(genericError, mockLogger, 'msg', {}, {})).toThrow(
        'Failed to perform operation on database.',
      );
    });

    it('should fall through to generic error for unhandled Postgres error codes', () => {
      const dbError = new Error('some other db error');
      // Set an unhandled Postgres error code (e.g., 42P01 - undefined_table)
      (dbError as any).code = '42P01';
      (dbError as any).constraint = 'some_constraint';
      (dbError as any).detail = 'Table does not exist';

      expect(() =>
        handleDbError(
          dbError,
          mockLogger,
          'Unknown DB error',
          { table: 'users' },
          { defaultMessage: 'Operation failed' },
        ),
      ).toThrow('Operation failed');

      // Verify logger.error was called with enhanced context including Postgres-specific fields
      expect(mockLogger.error).toHaveBeenCalledWith(
        expect.objectContaining({
          err: dbError,
          code: '42P01',
          constraint: 'some_constraint',
          detail: 'Table does not exist',
          table: 'users',
        }),
        'Unknown DB error',
      );
    });
  });
});
