import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext, CallHandler } from '@nestjs/common';
import { of } from 'rxjs';
import {
  LaravelTokenInterceptor,
  UseLaravelToken,
} from './laravel-token.interceptor';
import { AuthenticatedUser } from '../../auth/interfaces/jwt-payload.interface';

describe('LaravelTokenInterceptor', () => {
  let interceptor: LaravelTokenInterceptor;
  let mockExecutionContext: ExecutionContext;
  let mockCallHandler: CallHandler;
  let mockRequest: any;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [LaravelTokenInterceptor],
    }).compile();

    interceptor = module.get<LaravelTokenInterceptor>(LaravelTokenInterceptor);

    // Mock do request
    mockRequest = {
      user: null,
      laravelToken: null,
      laravelUserData: null,
    };

    // Mock do ExecutionContext
    mockExecutionContext = {
      switchToHttp: jest.fn().mockReturnValue({
        getRequest: jest.fn().mockReturnValue(mockRequest),
      }),
    } as any;

    // Mock do CallHandler
    mockCallHandler = {
      handle: jest.fn().mockReturnValue(of({ success: true })),
    };
  });

  it('should be defined', () => {
    expect(interceptor).toBeDefined();
  });

  describe('intercept', () => {
    it('should add laravelToken and laravelUserData when user has rawToken', (done) => {
      const mockUser: AuthenticatedUser = {
        id: '123',
        type: 'user',
        payload: {
          iss: 'test-issuer',
          iat: 1234567890,
          exp: 1234567890,
          nbf: 1234567890,
          sub: '123',
          jti: 'test-jti',
          prv: 'users',
        },
        expiresAt: new Date('2024-12-31'),
        issuedAt: new Date('2024-01-01'),
        tokenData: { role: 'admin' },
        rawToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
      };

      mockRequest.user = mockUser;

      const loggerSpy = jest.spyOn(interceptor['logger'], 'debug');

      interceptor.intercept(mockExecutionContext, mockCallHandler).subscribe({
        next: (response) => {
          expect(mockRequest.laravelToken).toBe(mockUser.rawToken);
          expect(mockRequest.laravelUserData).toEqual({
            id: mockUser.id,
            type: mockUser.type,
            tokenData: mockUser.tokenData,
            payload: mockUser.payload,
          });
          expect(loggerSpy).toHaveBeenCalledWith(
            `🔑 Token disponível para requisições Laravel: ${mockUser.type} ${mockUser.id}`,
          );
          expect(loggerSpy).toHaveBeenCalledWith(
            `✅ Requisição processada para usuário: ${mockUser.type} ${mockUser.id}`,
          );
          expect(response).toEqual({ success: true });
          done();
        },
        error: done,
      });
    });

    it('should not add laravelToken when user has no rawToken', (done) => {
      const mockUser: AuthenticatedUser = {
        id: '123',
        type: 'user',
        payload: {
          iss: 'test-issuer',
          iat: 1234567890,
          exp: 1234567890,
          nbf: 1234567890,
          sub: '123',
          jti: 'test-jti',
          prv: 'users',
        },
        expiresAt: new Date('2024-12-31'),
        issuedAt: new Date('2024-01-01'),
        tokenData: { role: 'admin' },
        rawToken: null,
      };

      mockRequest.user = mockUser;

      const loggerSpy = jest.spyOn(interceptor['logger'], 'debug');

      interceptor.intercept(mockExecutionContext, mockCallHandler).subscribe({
        next: (response) => {
          expect(mockRequest.laravelToken).toBeNull();
          expect(mockRequest.laravelUserData).toBeNull();
          expect(loggerSpy).not.toHaveBeenCalledWith(
            expect.stringContaining('🔑 Token disponível'),
          );
          expect(loggerSpy).toHaveBeenCalledWith(
            `✅ Requisição processada para usuário: ${mockUser.type} ${mockUser.id}`,
          );
          expect(response).toEqual({ success: true });
          done();
        },
        error: done,
      });
    });

    it('should handle request without user', (done) => {
      mockRequest.user = null;

      const loggerSpy = jest.spyOn(interceptor['logger'], 'debug');

      interceptor.intercept(mockExecutionContext, mockCallHandler).subscribe({
        next: (response) => {
          expect(mockRequest.laravelToken).toBeNull();
          expect(mockRequest.laravelUserData).toBeNull();
          expect(loggerSpy).not.toHaveBeenCalledWith(
            expect.stringContaining('🔑 Token disponível'),
          );
          expect(loggerSpy).not.toHaveBeenCalledWith(
            expect.stringContaining('✅ Requisição processada'),
          );
          expect(response).toEqual({ success: true });
          done();
        },
        error: done,
      });
    });

    it('should handle client type user', (done) => {
      const mockClient: AuthenticatedUser = {
        id: '456',
        type: 'client',
        payload: {
          iss: 'test-issuer',
          iat: 1234567890,
          exp: 1234567890,
          nbf: 1234567890,
          sub: '456',
          jti: 'test-jti',
          prv: 'clients',
        },
        expiresAt: new Date('2024-12-31'),
        issuedAt: new Date('2024-01-01'),
        tokenData: { permissions: ['read', 'write'] },
        rawToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
      };

      mockRequest.user = mockClient;

      const loggerSpy = jest.spyOn(interceptor['logger'], 'debug');

      interceptor.intercept(mockExecutionContext, mockCallHandler).subscribe({
        next: (response) => {
          expect(mockRequest.laravelToken).toBe(mockClient.rawToken);
          expect(mockRequest.laravelUserData).toEqual({
            id: mockClient.id,
            type: mockClient.type,
            tokenData: mockClient.tokenData,
            payload: mockClient.payload,
          });
          expect(loggerSpy).toHaveBeenCalledWith(
            `🔑 Token disponível para requisições Laravel: ${mockClient.type} ${mockClient.id}`,
          );
          expect(loggerSpy).toHaveBeenCalledWith(
            `✅ Requisição processada para usuário: ${mockClient.type} ${mockClient.id}`,
          );
          expect(response).toEqual({ success: true });
          done();
        },
        error: done,
      });
    });

    it('should preserve existing request properties', (done) => {
      const mockUser: AuthenticatedUser = {
        id: '123',
        type: 'user',
        payload: {
          iss: 'test-issuer',
          iat: 1234567890,
          exp: 1234567890,
          nbf: 1234567890,
          sub: '123',
          jti: 'test-jti',
          prv: 'users',
        },
        expiresAt: new Date('2024-12-31'),
        issuedAt: new Date('2024-01-01'),
        tokenData: { role: 'admin' },
        rawToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
      };

      // Adiciona propriedades existentes ao request
      mockRequest.existingProperty = 'should-be-preserved';
      mockRequest.user = mockUser;

      interceptor.intercept(mockExecutionContext, mockCallHandler).subscribe({
        next: (response) => {
          expect(mockRequest.existingProperty).toBe('should-be-preserved');
          expect(mockRequest.laravelToken).toBe(mockUser.rawToken);
          expect(mockRequest.laravelUserData).toBeDefined();
          done();
        },
        error: done,
      });
    });

    it('should handle errors in the response stream', (done) => {
      const mockUser: AuthenticatedUser = {
        id: '123',
        type: 'user',
        payload: {
          iss: 'test-issuer',
          iat: 1234567890,
          exp: 1234567890,
          nbf: 1234567890,
          sub: '123',
          jti: 'test-jti',
          prv: 'users',
        },
        expiresAt: new Date('2024-12-31'),
        issuedAt: new Date('2024-01-01'),
        tokenData: { role: 'admin' },
        rawToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
      };

      mockRequest.user = mockUser;

      // Mock do CallHandler que retorna erro usando throwError do RxJS
      const { throwError } = require('rxjs');
      const errorCallHandler = {
        handle: jest
          .fn()
          .mockReturnValue(throwError(() => new Error('Test error'))),
      };

      interceptor.intercept(mockExecutionContext, errorCallHandler).subscribe({
        next: () => {
          // Não deve chegar aqui
          done(new Error('Should not reach next'));
        },
        error: (error) => {
          expect(error.message).toBe('Test error');
          // Verifica se os dados foram adicionados antes do erro
          expect(mockRequest.laravelToken).toBe(mockUser.rawToken);
          expect(mockRequest.laravelUserData).toBeDefined();
          done();
        },
      });
    });
  });

  describe('UseLaravelToken decorator', () => {
    it('should return the descriptor unchanged', () => {
      const mockDescriptor = {
        value: jest.fn(),
        writable: true,
        enumerable: false,
        configurable: true,
      };

      const mockTarget = {};
      const mockPropertyKey = 'testMethod';

      const result = UseLaravelToken()(
        mockTarget,
        mockPropertyKey,
        mockDescriptor,
      );

      expect(result).toBe(mockDescriptor);
    });

    it('should work without descriptor', () => {
      const mockTarget = {};
      const mockPropertyKey = 'testMethod';

      const result = UseLaravelToken()(mockTarget, mockPropertyKey);

      expect(result).toBeUndefined();
    });

    it('should work with class decorator', () => {
      const mockTarget = class TestController {};

      const result = UseLaravelToken()(mockTarget);

      expect(result).toBeUndefined();
    });
  });
});
