/*
 * Copyright 2020-2025 Open Text.
 *
 * The only warranties for products and services of Open Text and
 * its affiliates and licensors (“Open Text”) are as may be set forth
 * in the express warranty statements accompanying such products and services.
 * Nothing herein should be construed as constituting an additional warranty.
 * Open Text shall not be liable for technical or editorial errors or
 * omissions contained herein. The information contained herein is subject
 * to change without notice.
 *
 * Except as specifically indicated otherwise, this document contains
 * confidential information and a valid license is required for possession,
 * use or copying. If this work is provided to the U.S. Government,
 * consistent with FAR 12.211 and 12.212, Commercial Computer Software,
 * Computer Software Documentation, and Technical Data for Commercial Items are
 * licensed to the U.S. Government under vendor's standard commercial license.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *   http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* eslint-env mocha */

import assert from 'assert';
import { AxiosInstance } from 'axios';
import MockAdapter from 'axios-mock-adapter';
import RequestHandler from '../../lib/root/requestHandler';
const testServerUrl = 'http://localhost';
const uri = '/some/uri';
const user = 'MyUser';
const password = 'MyPassword';
const token = 'TestToken123';

const UNAUTHORIZED_ERROR = {
  name: 'Error',
  response: {
    status: 401,
  },
};

export class BuildServerResponses {
  scope: MockAdapter;
  authenticatedCookie: string;
  loginCookieName: string;
  constructor(requester: AxiosInstance) {
    this.scope = new MockAdapter(requester);
    this.authenticatedCookie = 'Authenticated';
    this.loginCookieName = 'LWSSO_COOKIE_KEY';
  }

  withSuccessfulAuthentication(user?: string, password?: string) {
    this.scope
        .onPost('/authentication/sign_in', {
          asymmetricMatch(actual: { user: string; password: string }) {
            if (user) {
              assert.strictEqual(actual.user, user);
            }
            if (password) {
              assert.strictEqual(actual.password, password);
            }
            return true;
          },
        })
        .reply(200, undefined, {
          'Set-Cookie': this.loginCookieName + '=' + this.authenticatedCookie,
        });
    return this;
  }

  withUnsuccessfulAuthentication() {
    this.scope
        .onPost('/authentication/sign_in')
        .reply(401, undefined, { 'Set-Cookie': this.loginCookieName + '=' });
    return this;
  }

  withUnsuccessfulGetRequest(uri: string, errorBody: object) {
    this.scope.onGet(uri).reply(400, errorBody);
    return this;
  }

  withUnsuccessfulDeleteRequest(uri: string, errorBody: object) {
    this.scope.onDelete(uri).reply(400, errorBody);
    return this;
  }

  withUnsuccessfulUpdateRequest(uri: string, errorBody: object) {
    this.scope.onPut(uri).reply(400, errorBody);
    return this;
  }

  withUnsuccessfulCreateRequest(uri: string, errorBody: object) {
    this.scope.onPost(uri).reply(400, errorBody);
    return this;
  }

  withGetRequest(uri: string, statusCode: number, replyData?: any) {
    this.scope.onGet(uri).reply(statusCode, replyData);
    return this;
  }

  withDeleteRequest(uri: string, statusCode: number) {
    this.scope.onDelete(uri).reply(statusCode);
    return this;
  }

  withUpdateRequest(
      uri: string,
      statusCode: number,
      body?: object,
      replyData?: any
  ) {
    this.scope.onPut(uri, body).reply(statusCode, replyData);
    return this;
  }

  withCreateRequest(
      uri: string,
      statusCode: number,
      body?: object,
      replyData?: any
  ) {
    this.scope.onPost(uri, body).reply(statusCode, replyData);
    return this;
  }

  withUnauthorizedUpdateRequest(uri: string, body?: object, replyData?: any) {
    return this.withUpdateRequest(uri, 401, body, replyData);
  }

  withUnauthorizedGetRequest(uri: string) {
    return this.withGetRequest(uri, 401);
  }

  withUnauthorizedDeleteRequest(uri: string) {
    return this.withDeleteRequest(uri, 401);
  }

  withUnauthorizedCreateRequest(uri: string, body?: object, replyData?: any) {
    return this.withCreateRequest(uri, 401, body, replyData);
  }

  withSuccessfulSignOut() {
    this.scope
        .onPost('/authentication/sign_out')
        .reply(200, undefined, { 'Set-Cookie': this.loginCookieName + '=' });
    return this;
  }

  withUnsuccessfulSignOut() {
    this.scope
        .onPost('/authentication/sign_out')
        .reply(401, undefined, { 'Set-Cookie': 'LWSSO_COOKIE_KEY=' });
    return this;
  }

  withTokenGetRequest(uri: string, token: string, statusCode: number, replyData?: any) {
    this.scope
        .onGet(uri)
        .reply(function(config) {
          // Verify the Authorization header contains the token
          if (config.headers?.Authorization === `Bearer ${token}`) {
            return [statusCode, replyData];
          }
          return [401, { error: 'Invalid token' }];
        });
    return this;
  }

  withUnauthorizedTokenGetRequest(uri: string, token: string) {
    return this.withTokenGetRequest(uri, token, 401);
  }

  withTokenDeleteRequest(uri: string, token: string, statusCode: number) {
    this.scope
        .onDelete(uri)
        .reply(function(config) {
          if (config.headers?.Authorization === `Bearer ${token}`) {
            return [statusCode];
          }
          return [401, { error: 'Invalid token' }];
        });
    return this;
  }

  withUnauthorizedTokenDeleteRequest(uri: string, token: string) {
    return this.withTokenDeleteRequest(uri, token, 401);
  }

  withTokenUpdateRequest(uri: string, token: string, statusCode: number, body?: object, replyData?: any) {
    this.scope
        .onPut(uri, body)
        .reply(function(config) {
          if (config.headers?.Authorization === `Bearer ${token}`) {
            return [statusCode, replyData];
          }
          return [401, { error: 'Invalid token' }];
        });
    return this;
  }

  withTokenCreateRequest(uri: string, token: string, statusCode: number, body?: object, replyData?: any) {
    this.scope
        .onPost(uri, body)
        .reply(function(config) {
          if (config.headers?.Authorization === `Bearer ${token}`) {
            return [statusCode, replyData];
          }
          return [401, { error: 'Invalid token' }];
        });
    return this;
  }

  build() {
    return this.scope;
  }
}

describe('requestHandler', function () {
  // @ts-ignore
  this.slow(350);
  let requestHandler: RequestHandler;
  beforeEach(() => {
    const requestHandlerInitialization = {
      user: user,
      password: password,
      server: testServerUrl,
      sharedSpace: 1001,
      workspace: 1002,
    };
    requestHandler = new RequestHandler(requestHandlerInitialization);
  });

  async function throwsUnauthorizedException(promise: Promise<any>) {
    await throwsException(promise, 401, undefined);
  }

  async function throwsException(
    promise: Promise<any>,
    statusCode?: number,
    errorBody?: any
  ) {
    try {
      await promise;
      assert.fail();
    } catch (e: any) {
      if (errorBody) {
        assert.strictEqual(
          JSON.stringify(e.response.data),
          JSON.stringify(errorBody)
        );
      }
      if (statusCode) {
        assert.strictEqual(e.response.status, statusCode);
      }
    }
  }

  describe('#authenticate', () => {
    it('makes a successful authentication request with the given username and password', async () => {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withSuccessfulAuthentication(user, password)
        .build();
      const shouldBeUndefined = await requestHandler.authenticate();
      await assert.strictEqual(shouldBeUndefined.data, undefined);
      scope.reset();
    });
    it('throws an error after failed authentication', async () => {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnsuccessfulAuthentication()
        .build();
      await throwsUnauthorizedException(requestHandler.authenticate());
      scope.reset();
    });
  });

  describe('#get', () => {
    it('throws an error in case of an error response different from 401', async () => {
      const error = { error: 'Failed as intended' };
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnsuccessfulGetRequest(uri, error)
        .build();
      await throwsException(requestHandler.get(uri), 400, error);
      scope.reset();
    });
    it('throws an error if request was answered with status code 401 two times', async () => {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnauthorizedGetRequest(uri)
        .withSuccessfulAuthentication()
        .withUnauthorizedGetRequest(uri)
        .build();
      await throwsUnauthorizedException(requestHandler.get(uri));
      scope.reset();
    });
    it('throws an error if request was answered with status code 401 and authentication fails', async () => {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnauthorizedGetRequest(uri)
        .withUnsuccessfulAuthentication()
        .build();
      await throwsUnauthorizedException(requestHandler.get(uri));
      scope.reset();
    });
    it('returns the data even if the 1st request was with the status code 401', async function () {
      const objectToGet = 'Request was successful';
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnauthorizedGetRequest(uri)
        .withSuccessfulAuthentication()
        .withGetRequest(uri, 200, objectToGet)
        .build();
      const objectGot = await requestHandler.get(uri);
      assert.strictEqual(objectGot.data, objectToGet);
      scope.reset();
    });
    it('makes a successful get request', async function () {
      const objectToGet = 'Request was successful';
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withGetRequest(uri, 200, objectToGet)
        .build();
      const objectGot = await requestHandler.get(uri);
      assert.strictEqual(objectGot.data, objectToGet);
      scope.reset();
    });
  });

  describe('#delete', () => {
    it('throws an error in case of an error response different from 401', async () => {
      const error = { error: 'Failed as intended' };
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnsuccessfulDeleteRequest(uri, error)
        .build();
      await throwsException(requestHandler.delete(uri), 400, error);
      scope.reset();
    });
    it('throws an error if request was answered with status code 401 two times', async function () {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnauthorizedDeleteRequest(uri)
        .withSuccessfulAuthentication()
        .withUnauthorizedDeleteRequest(uri)
        .build();
      await throwsUnauthorizedException(requestHandler.delete(uri));
      scope.reset();
    });
    it('throws an error if request was answered with status code 401 and authentication fails', async () => {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnauthorizedDeleteRequest(uri)
        .withUnsuccessfulAuthentication()
        .build();
      await throwsUnauthorizedException(requestHandler.delete(uri));
      scope.reset();
    });
    it('request is made even if 1st request was with the status code 401', async function () {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnauthorizedDeleteRequest(uri)
        .withSuccessfulAuthentication()
        .withDeleteRequest(uri, 200)
        .build();
      const shouldBeUndefined = await requestHandler.delete(uri);
      assert.strictEqual(shouldBeUndefined?.data, undefined);
      scope.reset();
    });
    it('makes a successful delete request', async function () {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withDeleteRequest(uri, 200)
        .build();
      const shouldBeUndefined = await requestHandler.delete(uri);
      assert.strictEqual(shouldBeUndefined.data, undefined);
      scope.reset();
    });
  });

  describe('#update', () => {
    it('throws an error in case of an error response different from 401', async () => {
      const error = { error: 'Failed as intended' };
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnsuccessfulUpdateRequest(uri, error)
        .build();
      await throwsException(requestHandler.update(uri), 400, error);
      scope.reset();
    });
    it('throws an error if request was answered with status code 401 two times', async function () {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnauthorizedUpdateRequest(uri)
        .withSuccessfulAuthentication()
        .withUnauthorizedUpdateRequest(uri)
        .build();
      await throwsUnauthorizedException(requestHandler.update(uri));
      scope.reset();
    });
    it('throws an error if request was answered with status code 401 and authentication fails', async () => {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnauthorizedUpdateRequest(uri)
        .withUnsuccessfulAuthentication()
        .build();
      await throwsUnauthorizedException(requestHandler.update(uri));
      scope.reset();
    });
    it('request is made even if 1st request was with the status code 401', async function () {
      const updatedObject = 'Success';
      const updateBody = {
        parameter1: 'para 1',
        parameter2: 2,
      };
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnauthorizedUpdateRequest(uri, updateBody, updatedObject)
        .withSuccessfulAuthentication()
        .withUpdateRequest(uri, 200, updateBody, updatedObject)
        .build();

      const responseObject = await requestHandler.update(uri, updateBody);
      assert.strictEqual(responseObject.data, updatedObject);

      scope.reset();
    });
    it('makes a successful update request', async function () {
      const updatedObject = 'Success';
      const updateBody = {
        parameter1: 'para 1',
        parameter2: 2,
      };
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUpdateRequest(uri, 200, updateBody, updatedObject)
        .build();
      const responseObject = await requestHandler.update(uri, updateBody);
      assert.strictEqual(responseObject.data, updatedObject);
      scope.reset();
    });
  });

  describe('#create', () => {
    it('throws an error in case of an error response different from 401', async () => {
      const error = { error: 'Failed as intended' };
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnsuccessfulCreateRequest(uri, error)
        .build();
      await throwsException(requestHandler.create(uri), 400, error);
      scope.reset();
    });
    it('throws an error if request was answered with status code 401 two times', async function () {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnauthorizedCreateRequest(uri)
        .withSuccessfulAuthentication()
        .withUnauthorizedCreateRequest(uri)
        .build();
      await throwsUnauthorizedException(requestHandler.create(uri));
      scope.reset();
    });
    it('throws an error if request was answered with status code 401 and authentication fails', async () => {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnauthorizedCreateRequest(uri)
        .withUnsuccessfulAuthentication()
        .build();
      await throwsUnauthorizedException(requestHandler.create(uri));
      scope.reset();
    });
    it('request is made even if 1st request was with the status code 401', async function () {
      const createObject = 'Success';
      const createBody = {
        parameter1: 'para 1',
        parameter2: 2,
      };
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnauthorizedCreateRequest(uri, createBody, createObject)
        .withSuccessfulAuthentication()
        .withCreateRequest(uri, 200, createBody, createObject)
        .build();
      const responseObject = await requestHandler.create(uri, createBody);
      assert.strictEqual(responseObject.data, createObject);
      scope.reset();
    });
    it('makes a successful update request', async function () {
      const createObject = { data: 'Success' };
      const createBody = {
        parameter1: 'para 1',
        parameter2: 2,
      };
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withCreateRequest(uri, 200, createBody, createObject)
        .build();
      const responseObject = await requestHandler.create(uri, createBody);
      assert.strictEqual(
        JSON.stringify(responseObject.data),
        JSON.stringify(createObject)
      );
      scope.reset();
    });
  });

  describe('#reauthenicate', () => {
    it('throws the error given if it does not have the status code 401', async () => {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnsuccessfulAuthentication()
        .build();
      const errorWithoutStatusCode = { response: { data: 'Error' } };
      const errorWithStatusCode = { response: { status: 400, data: 'Error' } };
      await throwsException(
        // @ts-ignore
        requestHandler._reauthenticate(errorWithoutStatusCode),
        undefined,
        errorWithoutStatusCode.response.data
      );
      await throwsException(
        // @ts-ignore
        requestHandler._reauthenticate(errorWithStatusCode),
        400,
        errorWithStatusCode.response.data
      );
      scope.reset();
    });
    it('throws an error if the error received has status code 401 and the authentication fails ', async function () {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnsuccessfulAuthentication()
        .build();
      await throwsUnauthorizedException(
        // @ts-ignore
        requestHandler._reauthenticate(UNAUTHORIZED_ERROR)
      );
      scope.reset();
    });
    it('authenticates again if the received error has the status code 401', async function () {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withSuccessfulAuthentication()
        .build();
      // @ts-ignore
      const shouldBeUndefined = await requestHandler._reauthenticate(
        UNAUTHORIZED_ERROR
      );
      assert.strictEqual(shouldBeUndefined?.data, undefined);
      scope.reset();
    });
  });

  describe('#signOut', () => {
    it('throws an error if the request fails', async function () {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withUnsuccessfulSignOut()
        .build();
      await throwsUnauthorizedException(requestHandler.signOut());
      scope.reset();
    });
    it('throws an error if the request fails', async function () {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandler._requestor)
        .withSuccessfulSignOut()
        .build();
      const shouldBeUndefined = await requestHandler.signOut();
      assert.strictEqual(shouldBeUndefined.data, undefined);
      scope.reset();
    });
  });
});

describe('requestHandler with token authentication', function () {
  let requestHandlerWithToken: RequestHandler;

  beforeEach(() => {
    requestHandlerWithToken = new RequestHandler({
      token: token,
      server: testServerUrl,
      sharedSpace: 1001,
      workspace: 1002,
    });
  });

  describe('#get with token', () => {
    it('makes a successful get request using token authentication', async function () {
      const objectToGet = 'Request was successful with token';
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandlerWithToken._requestor)
          .withTokenGetRequest(uri, token, 200, objectToGet)
          .build();

      const objectGot = await requestHandlerWithToken.get(uri);
      assert.strictEqual(objectGot.data, objectToGet);
      scope.reset();
    });

    it('throws an error if token authentication fails', async function () {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandlerWithToken._requestor)
          .withUnauthorizedTokenGetRequest(uri, token)
          .build();

      try {
        await requestHandlerWithToken.get(uri);
        assert.fail('Expected an error to be thrown');
      } catch (err: any) {
        assert.ok(
            err.message.includes('Token authentication failed'),
            `Expected error message about token failure, but got: ${err.message}`
        );
      }
      scope.reset();
    });
  });

  describe('#delete with token', () => {
    it('makes a successful delete request using token authentication', async function () {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandlerWithToken._requestor)
          .withTokenDeleteRequest(uri, token, 200)
          .build();

      const shouldBeUndefined = await requestHandlerWithToken.delete(uri);
      assert.strictEqual(shouldBeUndefined.data, undefined);
      scope.reset();
    });

    it('throws an error if token authentication fails', async function () {
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandlerWithToken._requestor)
          .withUnauthorizedTokenDeleteRequest(uri, token)
          .build();

      try {
        await requestHandlerWithToken.delete(uri);
        assert.fail('Expected an error to be thrown');
      } catch (err: any) {
        assert.ok(
            err.message.includes('Token authentication failed'),
            `Expected error message about token failure, but got: ${err.message}`
        );
      }
      scope.reset();
    });
  });

  describe('#update with token', () => {
    it('makes a successful update request using token authentication', async function () {
      const updatedObject = 'Success';
      const updateBody = {
        parameter1: 'parameter1',
        parameter2: 2,
      };
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandlerWithToken._requestor)
          .withTokenUpdateRequest(uri, token, 200, updateBody, updatedObject)
          .build();

      const responseObject = await requestHandlerWithToken.update(uri, updateBody);
      assert.strictEqual(responseObject.data, updatedObject);
      scope.reset();
    });
  });

  describe('#create with token', () => {
    it('makes a successful create request using token authentication', async function () {
      const createObject = { data: 'Success' };
      const createBody = {
        parameter1: 'parameter1',
        parameter2: 2,
      };
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandlerWithToken._requestor)
          .withTokenCreateRequest(uri, token, 200, createBody, createObject)
          .build();

      const responseObject = await requestHandlerWithToken.create(uri, createBody);
      assert.strictEqual(
          JSON.stringify(responseObject.data),
          JSON.stringify(createObject)
      );
      scope.reset();
    });
  });
});

// Test the hybrid flow (credentials + token)
describe('requestHandler with credentials and token (hybrid)', function () {
  let requestHandlerHybrid: RequestHandler;

  beforeEach(() => {
    requestHandlerHybrid = new RequestHandler({
      user: user,
      password: password,
      token: token,
      server: testServerUrl,
      sharedSpace: 1001,
      workspace: 1002,
    });
  });

    it('check if credentials were specifically used when having hybrid setup', async function () {
      const objectToGet = 'Request was successful';
      // @ts-ignore
      const scope = new BuildServerResponses(requestHandlerHybrid._requestor)
          .withSuccessfulAuthentication()
          .withGetRequest(uri, 200, objectToGet)
          .build();
      const objectGot = await requestHandlerHybrid.get(uri);
      assert.strictEqual(objectGot.data, objectToGet);
      scope.reset();
    });
});
