
import { DyNTS_OAuth2_ControlService } from './oauth2.control-service';
import { DyNTS_OAuth2_AuthService } from './oauth2.auth-service';
import { Request, Response } from 'express';
import { DyFM_Error } from '@futdevpro/fsm-dynamo';
import { DyNTS_global_settings } from '../../../_collections/global-settings.const';

describe('| DyNTS_OAuth2_ControlService', () => {
  let service: DyNTS_OAuth2_ControlService;
  let mockAuthService: jasmine.SpyObj<DyNTS_OAuth2_AuthService>;
  let mockRequest: Partial<Request>;
  let mockResponse: Partial<Response>;
  let cryptoJsOrigLib: unknown;

  beforeAll(() => {
    const cjs = require('crypto-js');
    const ref = (cjs && (cjs as { cryptoJs?: unknown }).cryptoJs) || (cjs && (cjs as { default?: unknown }).default) || cjs;
    if (ref && typeof ref === 'object') {
      cryptoJsOrigLib = (ref as { lib?: unknown }).lib;
      (ref as { lib: unknown }).lib = {
        WordArray: {
          random: (_n?: number): { toString: () => string } => ({
            toString: (): string => 'mock-token-' + Math.random().toString(36).slice(2, 12),
          }),
        },
      };
    }
  });

  afterAll(() => {
    if (cryptoJsOrigLib !== undefined) {
      const cjs = require('crypto-js');
      const cryptoJsRef = (cjs && (cjs as { cryptoJs?: { lib?: unknown } }).cryptoJs) || cjs.default || cjs;
      if (cryptoJsRef && typeof cryptoJsRef === 'object') {
        (cryptoJsRef as { lib: unknown }).lib = cryptoJsOrigLib;
      }
    }
  });

  beforeEach(() => {
    // Reset singleton instances to prevent state leakage between tests
    (DyNTS_OAuth2_ControlService as any).instance = undefined;
    (DyNTS_OAuth2_AuthService as any).instance = undefined;

    // Mock the AuthService.getInstance() to prevent circular dependency
    mockAuthService = jasmine.createSpyObj('DyNTS_OAuth2_AuthService', [
      'getTokenFromRequest',
      'authenticate_token',
    ]);
    spyOn(DyNTS_OAuth2_AuthService, 'getInstance').and.returnValue(mockAuthService);

    // Now we can safely get the ControlService instance
    service = DyNTS_OAuth2_ControlService.getInstance();
    
    // Replace the authService with our mock (cryptoJs is patched at module level in beforeAll)
    (service as any).authService = mockAuthService;
    mockRequest = {
      query: {},
      body: {},
      headers: {},
    };
    mockResponse = {
      redirect: jasmine.createSpy('redirect'),
      json: jasmine.createSpy('json'),
      status: jasmine.createSpy('status').and.returnValue({
        send: jasmine.createSpy('send'),
      }),
    };

    // Clear all maps before each test
    (service as any).authorizationCodes.clear();
    (service as any).accessTokens.clear();
    (service as any).refreshTokens.clear();
    (service as any).clients.clear();
    (service as any).users.clear();
  });

  it('| should be a singleton instance', () => {
    const instance1 = DyNTS_OAuth2_ControlService.getInstance();
    const instance2 = DyNTS_OAuth2_ControlService.getInstance();

    expect(instance1).toBe(instance2);
    expect(instance1).toBeInstanceOf(DyNTS_OAuth2_ControlService);
  });

  it('| should have correct service name', () => {
    expect(service.serviceName).toBe('OAuth2ControlService');
  });

  describe('| registerClient', () => {
    it('| should register a new client', () => {
      const result = service.registerClient(
        'client-123',
        'secret-123',
        ['http://localhost:3000/callback'],
        ['read', 'write']
      );

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

    it('| should not register duplicate client', () => {
      service.registerClient('client-123', 'secret-123', ['http://localhost:3000/callback'], ['read']);
      const result = service.registerClient('client-123', 'secret-456', ['http://localhost:3000/callback'], ['write']);

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

  describe('| registerUser', () => {
    it('| should register a new user', () => {
      const result = service.registerUser('user-123', 'password-123', ['read', 'write']);

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

    it('| should not register duplicate user', () => {
      service.registerUser('user-123', 'password-123', ['read']);
      const result = service.registerUser('user-123', 'password-456', ['write']);

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

  describe('| handleAuthorizationRequest', () => {
    beforeEach(() => {
      service.registerClient(
        'client-123',
        'secret-123',
        ['http://localhost:3000/callback'],
        ['read', 'write']
      );
    });

    it('| should handle authorization code flow', async () => {
      mockRequest.query = {
        response_type: 'code',
        client_id: 'client-123',
        redirect_uri: 'http://localhost:3000/callback',
        scope: 'read write',
        state: 'state-123',
      };
      // Spy to bypass cryptoJs.lib in generateAuthorizationCode (crypto-js nem mockolható ebben a környezetben)
      spyOn(service as any, 'generateAuthorizationCode').and.returnValue(Promise.resolve('mock-code-123'));

      await service.handleAuthorizationRequest(mockRequest as Request, mockResponse as Response);

      expect(mockResponse.redirect).toHaveBeenCalled();
      const redirectUrl = (mockResponse.redirect as jasmine.Spy).calls.mostRecent().args[0];
      expect(redirectUrl).toContain('http://localhost:3000/callback');
      expect(redirectUrl).toContain('code=');
      expect(redirectUrl).toContain('state=state-123');
    });

    it('| should handle implicit flow', async () => {
      mockRequest.query = {
        response_type: 'token',
        client_id: 'client-123',
        redirect_uri: 'http://localhost:3000/callback',
        scope: 'read',
        state: 'state-123',
      };
      // Spy to bypass cryptoJs.lib in generateAccessToken (crypto-js nem mockolható ebben a környezetben)
      spyOn(service as any, 'generateAccessToken').and.returnValue(Promise.resolve('mock-access-token-123'));

      await service.handleAuthorizationRequest(mockRequest as Request, mockResponse as Response);

      expect(mockResponse.redirect).toHaveBeenCalled();
      const redirectUrl = (mockResponse.redirect as jasmine.Spy).calls.mostRecent().args[0];
      expect(redirectUrl).toContain('http://localhost:3000/callback');
      expect(redirectUrl).toContain('#access_token=');
    });

    it('| should throw error when missing required parameters', async () => {
      mockRequest.query = {
        response_type: 'code',
        // Missing client_id and redirect_uri
      };

      await expectAsync(
        service.handleAuthorizationRequest(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });

    it('| should throw error when client_id is invalid', async () => {
      mockRequest.query = {
        response_type: 'code',
        client_id: 'invalid-client',
        redirect_uri: 'http://localhost:3000/callback',
      };

      await expectAsync(
        service.handleAuthorizationRequest(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });

    it('| should throw error when redirect_uri is invalid', async () => {
      mockRequest.query = {
        response_type: 'code',
        client_id: 'client-123',
        redirect_uri: 'http://evil.com/callback',
      };

      await expectAsync(
        service.handleAuthorizationRequest(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });

    it('| should throw error when scope is invalid', async () => {
      mockRequest.query = {
        response_type: 'code',
        client_id: 'client-123',
        redirect_uri: 'http://localhost:3000/callback',
        scope: 'invalid-scope',
      };

      await expectAsync(
        service.handleAuthorizationRequest(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });

    it('| should throw error when response_type is unsupported', async () => {
      mockRequest.query = {
        response_type: 'unsupported',
        client_id: 'client-123',
        redirect_uri: 'http://localhost:3000/callback',
      };

      await expectAsync(
        service.handleAuthorizationRequest(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });
  });

  describe('| handleTokenRequest', () => {
    beforeEach(() => {
      service.registerClient(
        'client-123',
        'secret-123',
        ['http://localhost:3000/callback'],
        ['read', 'write']
      );
      // Bypass cryptoJs.lib (crypto-js nem mockolható ebben a környezetben)
      spyOn(service as any, 'generateAuthorizationCode').and.returnValue(Promise.resolve('mock-code'));
      spyOn(service as any, 'generateAccessToken').and.returnValue(Promise.resolve('mock-access-token'));
      spyOn(service as any, 'generateRefreshToken').and.returnValue(Promise.resolve('mock-refresh-token'));
    });

    it('| should handle refresh_token grant type', async () => {
      // First, get a refresh token
      const refreshToken = await (service as any).generateRefreshToken('client-123');
      (service as any).refreshTokens.set(refreshToken, {
        clientId: 'client-123',
        scope: 'read',
        accessToken: 'old-access-token',
      });

      mockRequest.body = {
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
        client_id: 'client-123',
        client_secret: 'secret-123',
      };
      mockResponse.json = jasmine.createSpy('json');

      await service.handleTokenRequest(mockRequest as Request, mockResponse as Response);

      expect(mockResponse.json).toHaveBeenCalled();
      const response = (mockResponse.json as jasmine.Spy).calls.mostRecent().args[0];
      expect(response.access_token).toBeDefined();
      expect(response.refresh_token).toBeDefined();
    });

    it('| should handle client_credentials grant type', async () => {
      mockRequest.body = {
        grant_type: 'client_credentials',
        client_id: 'client-123',
        client_secret: 'secret-123',
      };
      mockResponse.json = jasmine.createSpy('json');

      await service.handleTokenRequest(mockRequest as Request, mockResponse as Response);

      expect(mockResponse.json).toHaveBeenCalled();
      const response = (mockResponse.json as jasmine.Spy).calls.mostRecent().args[0];
      expect(response.access_token).toBeDefined();
      expect(response.token_type).toBe('Bearer');
    });

    it('| should handle password grant type', async () => {
      service.registerUser('user-123', 'password-123', ['read', 'write']);

      mockRequest.body = {
        grant_type: 'password',
        client_id: 'client-123',
        client_secret: 'secret-123',
        username: 'user-123',
        password: 'password-123',
      };
      mockResponse.json = jasmine.createSpy('json');

      await service.handleTokenRequest(mockRequest as Request, mockResponse as Response);

      expect(mockResponse.json).toHaveBeenCalled();
      const response = (mockResponse.json as jasmine.Spy).calls.mostRecent().args[0];
      expect(response.access_token).toBeDefined();
      expect(response.refresh_token).toBeDefined();
    });

    it('| should throw error when missing required parameters', async () => {
      mockRequest.body = {
        grant_type: 'authorization_code',
        // Missing client_id and client_secret
      };

      await expectAsync(
        service.handleTokenRequest(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });

    it('| should throw error when client credentials are invalid', async () => {
      mockRequest.body = {
        grant_type: 'authorization_code',
        client_id: 'client-123',
        client_secret: 'wrong-secret',
      };

      await expectAsync(
        service.handleTokenRequest(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });

    it('| should throw error when authorization code is missing', async () => {
      mockRequest.body = {
        grant_type: 'authorization_code',
        client_id: 'client-123',
        client_secret: 'secret-123',
        // Missing code
      };

      await expectAsync(
        service.handleTokenRequest(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });

    it('| should throw error when authorization code is invalid', async () => {
      mockRequest.body = {
        grant_type: 'authorization_code',
        code: 'invalid-code',
        client_id: 'client-123',
        client_secret: 'secret-123',
      };

      await expectAsync(
        service.handleTokenRequest(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });

    it('| should throw error when refresh token is missing', async () => {
      mockRequest.body = {
        grant_type: 'refresh_token',
        client_id: 'client-123',
        client_secret: 'secret-123',
        // Missing refresh_token
      };

      await expectAsync(
        service.handleTokenRequest(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });

    it('| should throw error when refresh token is invalid', async () => {
      mockRequest.body = {
        grant_type: 'refresh_token',
        refresh_token: 'invalid-refresh-token',
        client_id: 'client-123',
        client_secret: 'secret-123',
      };

      await expectAsync(
        service.handleTokenRequest(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });

    it('| should throw error when username or password is missing', async () => {
      mockRequest.body = {
        grant_type: 'password',
        client_id: 'client-123',
        client_secret: 'secret-123',
        // Missing username and password
      };

      await expectAsync(
        service.handleTokenRequest(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });

    it('| should throw error when user credentials are invalid', async () => {
      mockRequest.body = {
        grant_type: 'password',
        client_id: 'client-123',
        client_secret: 'secret-123',
        username: 'user-123',
        password: 'wrong-password',
      };

      await expectAsync(
        service.handleTokenRequest(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });

    it('| should throw error when grant_type is unsupported', async () => {
      mockRequest.body = {
        grant_type: 'unsupported',
        client_id: 'client-123',
        client_secret: 'secret-123',
      };

      await expectAsync(
        service.handleTokenRequest(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });
  });

  describe('| handleUserInfoRequest', () => {
    beforeEach(() => {
      service.registerClient(
        'client-123',
        'secret-123',
        ['http://localhost:3000/callback'],
        ['read', 'write']
      );
      // Don't spy here, let each test set up its own spy
    });

    // Skipped: Token validation fails even with manually created token
    it('| should return user info for valid token', async () => {
      // Manually create a token and store it in accessTokens map
      const accessToken = 'test-access-token-' + Date.now();
      const tokenData = {
        clientId: 'client-123',
        scope: 'profile email',
        expiresAt: Date.now() + 3600000, // 1 hour
      };
      (service as any).accessTokens.set(accessToken, tokenData);
      mockAuthService.getTokenFromRequest.and.returnValue(accessToken);
      mockResponse.json = jasmine.createSpy('json');

      await service.handleUserInfoRequest(mockRequest as Request, mockResponse as Response);

      expect(mockResponse.json).toHaveBeenCalled();
      const response = (mockResponse.json as jasmine.Spy).calls.mostRecent().args[0];
      expect(response.sub).toBeDefined();
      expect(response.name).toBeDefined();
      expect(response.email).toBeDefined();
    });

    it('| should throw error when token is missing', async () => {
      mockAuthService.getTokenFromRequest.and.throwError(new Error('Token missing'));

      await expectAsync(
        service.handleUserInfoRequest(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });

    it('| should throw error when token is invalid', async () => {
      mockAuthService.getTokenFromRequest.and.returnValue('invalid-token');

      await expectAsync(
        service.handleUserInfoRequest(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });

    it('| should throw error when token is expired', async () => {
      // Manually create an expired token
      const expiredToken = 'expired-token-' + Date.now();
      (service as any).accessTokens.set(expiredToken, {
        clientId: 'client-123',
        scope: 'read',
        expiresAt: Date.now() - 1000, // Expired
      });
      mockAuthService.getTokenFromRequest.and.returnValue(`Bearer ${expiredToken}`);

      await expectAsync(
        service.handleUserInfoRequest(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });
  });

  describe('| handleTokenRevocation', () => {
    beforeEach(() => {
      spyOn(service as any, 'generateAccessToken').and.returnValue(Promise.resolve('mock-access-token'));
      spyOn(service as any, 'generateRefreshToken').and.returnValue(Promise.resolve('mock-refresh-token'));
    });

    it('| should revoke access token', async () => {
      const accessToken = await (service as any).generateAccessToken('client-123', 'read');
      (service as any).accessTokens.set(accessToken, {
        clientId: 'client-123',
        scope: 'read',
        expiresAt: Date.now() + 3600000,
      });
      mockRequest.body = {
        token: accessToken,
        token_type_hint: 'access_token',
      };

      await service.handleTokenRevocation(mockRequest as Request, mockResponse as Response);

      expect(mockResponse.status).toHaveBeenCalledWith(200);
      expect((service as any).accessTokens.has(accessToken)).toBe(false);
    });

    it('| should revoke refresh token and associated access token', async () => {
      const accessToken = await (service as any).generateAccessToken('client-123', 'read');
      (service as any).accessTokens.set(accessToken, {
        clientId: 'client-123',
        scope: 'read',
        expiresAt: Date.now() + 3600000,
      });
      const refreshToken = await (service as any).generateRefreshToken('client-123');
      (service as any).refreshTokens.set(refreshToken, {
        clientId: 'client-123',
        scope: 'read',
        accessToken: accessToken,
      });

      mockRequest.body = {
        token: refreshToken,
        token_type_hint: 'refresh_token',
      };

      await service.handleTokenRevocation(mockRequest as Request, mockResponse as Response);

      expect(mockResponse.status).toHaveBeenCalledWith(200);
      expect((service as any).refreshTokens.has(refreshToken)).toBe(false);
      expect((service as any).accessTokens.has(accessToken)).toBe(false);
    });

    it('| should throw error when token is missing', async () => {
      mockRequest.body = {};

      await expectAsync(
        service.handleTokenRevocation(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });

    it('| should throw error when token is not found', async () => {
      mockRequest.body = {
        token: 'non-existent-token',
      };

      await expectAsync(
        service.handleTokenRevocation(mockRequest as Request, mockResponse as Response)
      ).toBeRejected();
    });
  });

  describe('| getAccessTokenData', () => {
    it('| should return access token data when token exists', async () => {
      // Manually create a token and store it in accessTokens map
      const accessToken = 'test-access-token-' + Date.now();
      const tokenData = {
        clientId: 'client-123',
        scope: 'read',
        expiresAt: Date.now() + 3600000, // 1 hour
      };
      (service as any).accessTokens.set(accessToken, tokenData);

      const result = service.getAccessTokenData(accessToken);

      expect(result).toBeDefined();
      expect(result?.clientId).toBe('client-123');
      expect(result?.scope).toBe('read');
      expect(result?.expiresAt).toBeGreaterThan(Date.now());
    });

    it('| should return undefined when token does not exist', () => {
      const tokenData = service.getAccessTokenData('non-existent-token');

      expect(tokenData).toBeUndefined();
    });
  });
});

