/*
 * Copyright (c) 2022-2024 Huawei Device Co., Ltd.
 * 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.
 */

import ExtendInterface from './ExtendInterface';
import VerificationMode from './VerificationMode';
import ArgumentMatchers from './ArgumentMatchers';

class MockKit {
  constructor() {
    this.mFunctions = [];
    this.stubs = new Map();
    this.recordCalls = new Map();
    this.currentSetKey = new Map();
    this.mockObj = null;
    this.recordMockedMethod = new Map();
  }

  init() {
    this.reset();
  }

  reset() {
    this.mFunctions = [];
    this.stubs = {};
    this.recordCalls = {};
    this.currentSetKey = new Map();
    this.mockObj = null;
    this.recordMockedMethod = new Map();
  }

  clearAll() {
    this.reset();
    var props = Object.keys(this);
    for (var i = 0; i < props.length; i++) {
      delete this[props[i]];
    }

    var props = Object.getOwnPropertyNames(this);
    for (var i = 0; i < props.length; i++) {
      delete this[props[i]];
    }
    for (var key in this) {
      delete this[key];
    }
  }

  clear(obj) {
    if (!obj) {
      throw Error('Please enter an object to be cleaned');
    }
    if (typeof obj !== 'object' && typeof obj !== 'function') {
      throw new Error('Not a object or static class');
    }
    this.recordMockedMethod.forEach(function (value, key, map) {
      if (key) {
        obj[key] = value;
      }
    });
  }

  ignoreMock(obj, method) {
    if (typeof obj !== 'object' && typeof obj !== 'function') {
      throw new Error('Not a object or static class');
    }
    if (typeof method !== 'function') {
      throw new Error('Not a function');
    }
    let og = this.recordMockedMethod.get(method.propName);
    if (og) {
      obj[method.propName] = og;
      this.recordMockedMethod.set(method.propName, undefined);
    }
  }

  extend(dest, source) {
    dest['stub'] = source['stub'];
    dest['afterReturn'] = source['afterReturn'];
    dest['afterReturnNothing'] = source['afterReturnNothing'];
    dest['afterAction'] = source['afterAction'];
    dest['afterThrow'] = source['afterThrow'];
    dest['stubMockedCall'] = source['stubMockedCall'];
    dest['clear'] = source['clear'];
    return dest;
  }

  stubApply(f, params, returnInfo) {
    let values = this.stubs.get(f);
    if (!values) {
      values = new Map();
    }
    let key = params[0];
    if (typeof key === 'undefined') {
      key = 'anonymous-mock-' + f.propName;
    } else {
      key = [];
      const matcher = new ArgumentMatchers();
      for (let i = 0; i < params.length; i++) {
        const param = params[i];
        const matchKey = matcher.matcheStubKey(param);
        if (matchKey) {
          key.push(matchKey);
        } else {
          key.push(param);
        }
      }
    }
    this.currentSetKey.set(f, key);
    values.set(key, returnInfo);
    this.stubs.set(f, values);
  }

  getReturnInfo(f, params) {
    let values = this.stubs.get(f);
    if (!values) {
      return undefined;
    }
    let returnKet = params[0];
    const anonymousName = 'anonymous-mock-' + f.propName;
    if (typeof returnKet === 'undefined') {
      returnKet = anonymousName;
      let stubSetKey = this.currentSetKey.get(f);

      if (stubSetKey && (typeof (returnKet) !== 'undefined')) {
        returnKet = stubSetKey;
      }
    } else {
      returnKet = this.getReturnKet(values, params, returnKet, anonymousName);
    }

    return values.get(returnKet);
  }

  getReturnKet(values, params, defaultValue, anonymousName) {
    let flag = true;
    let returnKet = defaultValue;
    values.forEach((value, paramsKey, map) => {
      if (
        flag &&
        paramsKey !== anonymousName &&
        paramsKey.length === params.length &&
        this.checkIsRightValue(paramsKey, params)
      ) {
        returnKet = paramsKey;
        flag = false;
      }
    });
    return returnKet;
  }

  checkIsRightValue(paramsKey, params) {
    const matcher = new ArgumentMatchers();
    return paramsKey.every((key, j) => {
      if (ArgumentMatchers.isRegExp(key) && typeof params[j] === 'string') {
        return key.test(params[j]);
      }
      const matchKey = matcher.matcheReturnKey(params[j], undefined, key);
      if (matchKey && matchKey === key) {
        return true;
      } else if (this.checkIsEqual(params[j], key)) {
        return true;
      }
      return false;
    });
  }

  checkIsEqual(value1, value2) {
    if (value1 === value2) {
      return true;
    }
    if (typeof value1 !== typeof value2) {
      return false;
    }
    if (Array.isArray(value1) && Array.isArray(value2)) {
      if (value1.length !== value2.length) {
        return false;
      }
      for (let i = 0; i < value1.length; i++) {
        if (!this.checkIsEqual(value1[i], value2[i])) {
          return false;
        }
      }
      return true;
    }
    if (Object.prototype.toString.call(value1) === '[object Object]' &&
      Object.prototype.toString.call(value2) === '[object Object]') {
      const keys1 = Object.keys(value1);
      const keys2 = Object.keys(value2);
      if (keys1.length !== keys2.length) {
        return false;
      }
      for (let key of keys1) {
        if (!keys2.includes(key) || !this.checkIsEqual(value1[key], value2[key])) {
          return false;
        }
      }
      return true;
    }
    if (Object.prototype.toString.call(value1) === '[object Date]' &&
      Object.prototype.toString.call(value2) === '[object Date]' &&
      value1.getTime() === value2.getTime()) {
      return true;
    }
    if (Object.prototype.toString.call(value1) === '[object RegExp]' &&
      Object.prototype.toString.call(value2) === '[object RegExp]' &&
      value1.toString() === value2.toString()) {
      return true;
    }
    return false;
  }

  findName(obj, value) {
    let properties = this.findProperties(obj);
    let name = null;
    properties
      .filter((item) => item !== 'caller' && item !== 'arguments')
      .forEach(function (va1, idx, array) {
        if (obj[va1] === value) {
          name = va1;
        }
      });
    return name;
  }

  isFunctionFromPrototype(f, container, propName) {
    if (
      container.constructor !== Object &&
      container.constructor.prototype !== container
    ) {
      return container.constructor.prototype[propName] === f;
    }
    return false;
  }

  findProperties(obj, ...arg) {
    function getProperty(newObj) {
      if (newObj.__proto__ === null) {
        return [];
      }
      let properties = Object.getOwnPropertyNames(newObj);
      return [...properties, ...getProperty(newObj.__proto__)];
    }
    return getProperty(obj);
  }

  recordMethodCall(originalMethod, args) {
    Function.prototype.getName = function () {
      return this.name || this.toString().match(/function\s*([^(]*)\(/)[1];
    };
    let name = originalMethod.getName();
    let arglistString = name + '(' + Array.from(args).toString() + ')';
    let records = this.recordCalls.get(arglistString);
    if (!records) {
      records = 0;
    }
    records++;
    this.recordCalls.set(arglistString, records);
  }

  mockFunc(originalObject, originalMethod) {
    let tmp = this;
    this.originalMethod = originalMethod;
    let f = function () {
      let args = arguments;
      let action = tmp.getReturnInfo(f, args);
      if (originalMethod) {
        tmp.recordMethodCall(originalMethod, args);
      }
      if (action) {
        return action.apply(this, args);
      }
    };

    f.container = null || originalObject;
    f.original = originalMethod || null;

    if (originalObject && originalMethod) {
      if (typeof originalMethod !== 'function') {
        throw new Error('Not a function');
      }
      var name = this.findName(originalObject, originalMethod);
      originalObject[name] = f;
      this.recordMockedMethod.set(name, originalMethod);
      f.propName = name;
      f.originalFromPrototype = this.isFunctionFromPrototype(
        f.original,
        originalObject,
        f.propName
      );
    }
    f.mocker = this;
    this.mFunctions.push(f);
    this.extend(f, new ExtendInterface(this));
    return f;
  }

  verify(methodName, argsArray) {
    if (!methodName) {
      throw Error('not a function name');
    }
    let a = this.recordCalls.get(methodName + '(' + argsArray.toString() + ')');
    return new VerificationMode(a ? a : 0);
  }

  mockObject(object) {
    if (!object || typeof object === 'string') {
      throw Error(`this ${object} cannot be mocked`);
    }
    const _this = this;
    let mockedObject = {};
    let keys = Reflect.ownKeys(object);
    keys
      .filter((key) => typeof Reflect.get(object, key) === 'function')
      .forEach((key) => {
        mockedObject[key] = object[key];
        mockedObject[key] = _this.mockFunc(mockedObject, mockedObject[key]);
      });
    return mockedObject;
  }
}

function ifMockedFunction(f) {
  if (
    Object.prototype.toString.call(f) !== '[object Function]' &&
    Object.prototype.toString.call(f) !== '[object AsyncFunction]'
  ) {
    throw Error('not a function');
  }
  if (!f.stub) {
    throw Error('not a mock function');
  }
  return true;
}

function when(f) {
  if (!ifMockedFunction(f)) {
    throw Error('not a mock function');
  }
  return f.stub.bind(f);
}

export { MockKit, when };
