import { BotError, ErrorCategory } from '../../src/errors/BotError';
import { ErrorCodes, createBotError } from '../../src/errors/ErrorCodes';
import { ErrorReporter } from '../../src/errors/ErrorReporter';

// Mock logger to prevent console output during tests
jest.mock('../../src/utils/logger', () => ({
  logger: {
    info: jest.fn(),
    warn: jest.fn(),
    error: jest.fn(),
    debug: jest.fn(),
  },
}));

describe('ErrorReporter', () => {
  describe('wrap()', () => {
    it('should return BotError as-is if already a BotError', () => {
      const original = createBotError(ErrorCodes.INVALID_USER_LIMIT);
      const wrapped = ErrorReporter.wrap(original);

      expect(wrapped).toBe(original);
      expect(wrapped.code).toBe('E1001');
    });

    it('should map Discord error code 50013 to DISCORD_MISSING_PERMISSIONS', () => {
      const discordError = new Error('Missing Permissions') as any;
      discordError.code = 50013;

      const wrapped = ErrorReporter.wrap(discordError);

      expect(wrapped).toBeInstanceOf(BotError);
      expect(wrapped.code).toBe('E4002');
      expect(wrapped.category).toBe(ErrorCategory.DISCORD_API);
      expect(wrapped.cause).toBe(discordError);
    });

    it('should map Discord error code 50001 to BOT_MISSING_MANAGE_CHANNELS', () => {
      const discordError = new Error('Missing Access') as any;
      discordError.code = 50001;

      const wrapped = ErrorReporter.wrap(discordError);

      expect(wrapped).toBeInstanceOf(BotError);
      expect(wrapped.code).toBe('E2001');
      expect(wrapped.category).toBe(ErrorCategory.PERMISSION);
    });

    it('should map HTTP 429 to DISCORD_RATE_LIMITED', () => {
      const rateLimitError = new Error('Rate Limited') as any;
      rateLimitError.httpStatus = 429;

      const wrapped = ErrorReporter.wrap(rateLimitError);

      expect(wrapped).toBeInstanceOf(BotError);
      expect(wrapped.code).toBe('E4001');
      expect(wrapped.category).toBe(ErrorCategory.DISCORD_API);
    });

    it('should wrap unknown errors as UNKNOWN_ERROR', () => {
      const unknownError = new Error('Something unexpected');
      const wrapped = ErrorReporter.wrap(unknownError);

      expect(wrapped).toBeInstanceOf(BotError);
      expect(wrapped.code).toBe('E3999');
      expect(wrapped.category).toBe(ErrorCategory.BOT_INTERNAL);
      expect(wrapped.cause).toBe(unknownError);
    });

    it('should handle non-Error objects (e.g. strings)', () => {
      const wrapped = ErrorReporter.wrap('string error');

      expect(wrapped).toBeInstanceOf(BotError);
      expect(wrapped.code).toBe('E3999');
    });
  });

  describe('report()', () => {
    it('should reply with an ephemeral embed when interaction has not been replied', async () => {
      const mockInteraction = {
        replied: false,
        deferred: false,
        reply: jest.fn().mockResolvedValue(undefined),
        followUp: jest.fn().mockResolvedValue(undefined),
      } as any;

      const error = createBotError(ErrorCodes.NOT_CHANNEL_OWNER);

      await ErrorReporter.report(mockInteraction, error, 'en');

      expect(mockInteraction.reply).toHaveBeenCalledTimes(1);
      expect(mockInteraction.followUp).not.toHaveBeenCalled();

      const replyArgs = mockInteraction.reply.mock.calls[0][0];
      expect(replyArgs.ephemeral).toBe(true);
      expect(replyArgs.embeds).toHaveLength(1);

      // Verify embed does NOT contain error code
      const embed = replyArgs.embeds[0];
      const embedJSON = embed.toJSON();
      expect(embedJSON.description).not.toContain('E2004');
      expect(embedJSON.footer).toBeUndefined();
    });

    it('should followUp when interaction has already been replied', async () => {
      const mockInteraction = {
        replied: true,
        deferred: false,
        reply: jest.fn().mockResolvedValue(undefined),
        followUp: jest.fn().mockResolvedValue(undefined),
      } as any;

      const error = createBotError(ErrorCodes.DATABASE_ERROR);

      await ErrorReporter.report(mockInteraction, error, 'en');

      expect(mockInteraction.followUp).toHaveBeenCalledTimes(1);
      expect(mockInteraction.reply).not.toHaveBeenCalled();
    });

    it('should include localized resolution field in embed when available', async () => {
      const mockInteraction = {
        replied: false,
        deferred: false,
        reply: jest.fn().mockResolvedValue(undefined),
      } as any;

      const error = createBotError(ErrorCodes.INVALID_USER_LIMIT);

      await ErrorReporter.report(mockInteraction, error, 'en');

      const embed = mockInteraction.reply.mock.calls[0][0].embeds[0];
      const embedJSON = embed.toJSON();
      expect(embedJSON.fields).toHaveLength(1);
      expect(embedJSON.fields[0].name).toBe('💡 How to resolve');
    });

    it('should render Korean messages when locale is ko', async () => {
      const mockInteraction = {
        replied: false,
        deferred: false,
        reply: jest.fn().mockResolvedValue(undefined),
      } as any;

      const error = createBotError(ErrorCodes.INVALID_USER_LIMIT);

      await ErrorReporter.report(mockInteraction, error, 'ko');

      const embed = mockInteraction.reply.mock.calls[0][0].embeds[0];
      const embedJSON = embed.toJSON();
      expect(embedJSON.title).toContain('입력을 확인해주세요');
      expect(embedJSON.description).toBe('사용자 제한 값이 올바르지 않습니다.');
      expect(embedJSON.fields[0].name).toBe('💡 해결 방법');
    });

    it('should not throw even if reply fails', async () => {
      const mockInteraction = {
        replied: false,
        deferred: false,
        reply: jest.fn().mockRejectedValue(new Error('Network error')),
      } as any;

      const error = createBotError(ErrorCodes.UNKNOWN_ERROR);

      // Should not throw
      await expect(ErrorReporter.report(mockInteraction, error, 'en')).resolves.not.toThrow();
    });
  });
});

describe('createBotError()', () => {
  it('should create a fresh BotError instance from a definition', () => {
    const error1 = createBotError(ErrorCodes.DATABASE_ERROR);
    const error2 = createBotError(ErrorCodes.DATABASE_ERROR);

    expect(error1).not.toBe(error2);
    expect(error1.code).toBe(error2.code);
    expect(error1.messageKey).toBe(error2.messageKey);
  });

  it('should attach a cause error', () => {
    const cause = new Error('Connection refused');
    const error = createBotError(ErrorCodes.DATABASE_ERROR, cause);

    expect(error.cause).toBe(cause);
  });
});
