import { describe, expect, it } from 'vitest';
import {
  BadRequestError,
  ConflictError,
  ForbiddenError,
  NotFoundError,
  ServiceError,
  TooManyRequestsError,
  UnauthorizedError,
} from '../index.js';

describe('ServiceError', () => {
  it('sets statusCode and message', () => {
    const err = new ServiceError(500, 'boom');
    expect(err.statusCode).toBe(500);
    expect(err.message).toBe('boom');
    expect(err).toBeInstanceOf(Error);
    expect(err).toBeInstanceOf(ServiceError);
  });

  it('supports optional details', () => {
    const err = new ServiceError(500, 'boom', { field: 'name' });
    expect(err.details).toEqual({ field: 'name' });
  });
});

describe('HTTP error classes', () => {
  const cases: [string, new () => ServiceError, number, string][] = [
    ['BadRequestError', BadRequestError, 400, 'Bad request'],
    ['UnauthorizedError', UnauthorizedError, 401, 'Unauthorized'],
    ['ForbiddenError', ForbiddenError, 403, 'Forbidden'],
    ['NotFoundError', NotFoundError, 404, 'Not found'],
    ['ConflictError', ConflictError, 409, 'Conflict'],
    ['TooManyRequestsError', TooManyRequestsError, 429, 'Too many requests'],
  ];

  for (const [name, Ctor, expectedStatus, expectedMessage] of cases) {
    it(`${name} has status ${expectedStatus}`, () => {
      const err = new Ctor();
      expect(err.statusCode).toBe(expectedStatus);
      expect(err.message).toBe(expectedMessage);
      expect(err).toBeInstanceOf(ServiceError);
    });
  }

  it('accepts custom message', () => {
    const err = new NotFoundError('User not found');
    expect(err.message).toBe('User not found');
    expect(err.statusCode).toBe(404);
  });

  it('accepts details', () => {
    const err = new TooManyRequestsError('Rate limited', { retryAfter: 60 });
    expect(err.details).toEqual({ retryAfter: 60 });
  });
});

describe('cross-instance detection (duplicated module copy)', () => {
  // Simulate a SECOND physical copy of @bytelyst/errors: a structurally
  // identical class that brands itself with the same global symbol but has a
  // different prototype chain (so plain prototype-based instanceof would fail).
  const BRAND = Symbol.for('@bytelyst/errors.ServiceError');
  class DuplicatedServiceError extends Error {
    constructor(
      public statusCode: number,
      message: string
    ) {
      super(message);
      this.name = 'ServiceError';
      Object.defineProperty(this, BRAND, { value: true, enumerable: false });
    }
  }

  it('recognizes a ServiceError created by a duplicated copy', () => {
    const err = new DuplicatedServiceError(409, 'conflict');
    expect(err).toBeInstanceOf(ServiceError);
    // Sanity: prototype chain genuinely differs (this is the bug scenario).
    expect(Object.getPrototypeOf(err)).not.toBe(ServiceError.prototype);
  });

  it('does not treat plain objects or non-branded errors as ServiceError', () => {
    expect(new Error('plain') instanceof ServiceError).toBe(false);
    expect({ statusCode: 500 } instanceof ServiceError).toBe(false);
    expect((null as unknown) instanceof ServiceError).toBe(false);
  });

  it('preserves subclass instanceof semantics', () => {
    const notFound = new NotFoundError();
    expect(notFound instanceof NotFoundError).toBe(true);
    expect(notFound instanceof ServiceError).toBe(true);
    // A NotFoundError must NOT be an instance of an unrelated sibling subclass.
    expect(notFound instanceof BadRequestError).toBe(false);
  });
});
