1/*
2 * Copyright (c) 2023 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import ExtendInterface from "./ExtendInterface.js";
17import VerificationMode from "./VerificationMode.js";
18import { ArgumentMatchers } from "./ArgumentMatchers.js";
19
20interface IFunction extends Function {
21    container: any;
22    original: any;
23    propName: string;
24    originalFromPrototype: boolean
25    mocker: MockKit
26}
27
28class MockKit {
29
30    private mFunctions:Array<any> = [];
31    private stubs = new Map();
32    private recordCalls = new Map();
33    private currentSetKey = new Map();
34    private mockObj = null;
35    private recordMockedMethod = new Map();
36    private originalMethod: any;
37
38    constructor() {
39        this.mFunctions = [];
40        this.stubs = new Map();
41        this.recordCalls = new Map();
42        this.currentSetKey = new Map();
43        this.mockObj = null;
44        this.recordMockedMethod = new Map();
45    }
46
47    init() {
48        this.reset();
49    }
50
51    reset() {
52        this.mFunctions = [];
53        this.stubs = new Map()
54        this.recordCalls = new Map();
55        this.currentSetKey = new Map();
56        this.mockObj = null;
57        this.recordMockedMethod = new Map();
58    }
59
60    clearAll() {
61        this.reset();
62    }
63
64    clear(obj: any) {
65        if (!obj) throw Error("Please enter an object to be cleaned");
66        if (typeof (obj) != 'object') throw new Error('Not a object');
67        this.recordMockedMethod.forEach(function (value, key, map) {
68            if (key) {
69                obj[key] = value;
70            }
71        });
72    }
73
74    ignoreMock(obj:any, method: any) {
75        if (typeof (obj) != 'object') throw new Error('Not a object');
76        if (typeof (method) != 'function') throw new Error('Not a function');
77        let og = this.recordMockedMethod.get(method.propName);
78        if (og) {
79            obj[method.propName] = og;
80            this.recordMockedMethod.set(method.propName, undefined);
81        }
82    }
83
84    extend(dest: any, source:any) {
85        dest["stub"] = source["stub"];
86        dest["afterReturn"] = source["afterReturn"];
87        dest["afterReturnNothing"] = source["afterReturnNothing"];
88        dest["afterAction"] = source["afterAction"];
89        dest["afterThrow"] = source["afterThrow"];
90        dest["stubMockedCall"] = source["stubMockedCall"];
91        dest["clear"] = source["clear"];
92        return dest;
93    }
94
95    stubApply(f: any, params:any, returnInfo:any) {
96        let values = this.stubs.get(f);
97        if (!values) {
98            values = new Map();
99        }
100        let key = params[0];
101        if (typeof key == "undefined") {
102            key = "anonymous-mock-" + f.propName;
103        }
104        let matcher = new ArgumentMatchers();
105        if (matcher.matcheStubKey(key)) {
106            key = matcher.matcheStubKey(key);
107            if (key) {
108                this.currentSetKey.set(f, key);
109            }
110        }
111        values.set(key, returnInfo);
112        this.stubs.set(f, values);
113    }
114
115    getReturnInfo(f: any, params:any) {
116        let values = this.stubs.get(f);
117        if (!values) {
118            return undefined;
119        }
120        let retrunKet = params[0];
121        if (typeof retrunKet == "undefined") {
122            retrunKet = "anonymous-mock-" + f.propName;
123        }
124        let stubSetKey = this.currentSetKey.get(f);
125
126        if (stubSetKey && (typeof (retrunKet) != "undefined")) {
127            retrunKet = stubSetKey;
128        }
129        let matcher = new ArgumentMatchers();
130        if (matcher.matcheReturnKey(params[0], undefined, stubSetKey) && matcher.matcheReturnKey(params[0], undefined, stubSetKey) != stubSetKey) {
131            retrunKet = params[0];
132        }
133
134        values.forEach(function (value: any, key: any, map: any) {
135            if (ArgumentMatchers.isRegExp(key) && matcher.matcheReturnKey(params[0], key)) {
136                retrunKet = key;
137            }
138        });
139
140        return values.get(retrunKet);
141    }
142
143    findName(obj: any, value: any) {
144        let properties = this.findProperties(obj);
145        let name = '';
146        properties.filter((item:any) => (item !== 'caller' && item !== 'arguments')).forEach(
147            function (va1:any, idx:any, array:any) {
148                if (obj[va1] === value) {
149                    name = va1;
150                }
151            }
152        );
153        return name;
154    }
155
156    isFunctionFromPrototype(f: Function, container:Function, propName: string) {
157        if (container.constructor != Object && container.constructor.prototype !== container) {
158            return container.constructor.prototype[propName] === f;
159        }
160        return false;
161    }
162
163    findProperties(obj: any, ...arg: Array<any>) {
164        function getProperty(new_obj:any): Array<any> {
165            if (new_obj.__proto__ === null) {
166                return [];
167            }
168            let properties = Object.getOwnPropertyNames(new_obj);
169            return [...properties, ...getProperty(new_obj.__proto__)];
170        }
171        return getProperty(obj);
172    }
173
174    recordMethodCall(originalMethod: any, args: any) {
175        originalMethod['getName'] = function () {
176            return this.name || this.toString().match(/function\s*([^(]*)\(/)[1];
177        }
178        let name = originalMethod.getName();
179        let arglistString = name + '(' + Array.from(args).toString() + ')';
180        let records = this.recordCalls.get(arglistString);
181        if (!records) {
182            records = 0;
183        }
184        records++;
185        this.recordCalls.set(arglistString, records);
186    }
187
188    mockFunc(originalObject:any, originalMethod:any) {
189        let tmp = this;
190        this.originalMethod = originalMethod;
191        const _this = this;
192        let f:any  = function () {
193            let args = arguments;
194            let action = tmp.getReturnInfo(f, args);
195            if (originalMethod) {
196                tmp.recordMethodCall(originalMethod, args);
197            }
198            if (action) {
199                return <IFunction> action.apply(_this, args);
200            }
201        };
202
203        f.container = null || originalObject;
204        f.original = originalMethod || null;
205
206        if (originalObject && originalMethod) {
207            if (typeof (originalMethod) != 'function') throw new Error('Not a function');
208            var name = this.findName(originalObject, originalMethod);
209            originalObject[name] = f;
210            this.recordMockedMethod.set(name, originalMethod);
211            f.propName = name;
212            f.originalFromPrototype = this.isFunctionFromPrototype(f.original, originalObject, f.propName);
213        }
214        f.mocker = this;
215        this.mFunctions.push(f);
216        this.extend(f, new ExtendInterface(this));
217        return f;
218    }
219
220    verify(methodName:any, argsArray:any) {
221        if (!methodName) {
222            throw Error("not a function name");
223        }
224        let a = this.recordCalls.get(methodName + '(' + argsArray.toString() + ')');
225        return new VerificationMode(a ? a : 0);
226    }
227
228    mockObject(object: any) {
229        if (!object || typeof object === "string") {
230            throw Error(`this ${object} cannot be mocked`);
231        }
232        const _this = this;
233        let mockedObject:any = {};
234        let keys = Reflect.ownKeys(object);
235        keys.filter(key => (typeof Reflect.get(object, key)) === 'function')
236            .forEach((key:any) => {
237                mockedObject[key] = object[key];
238                mockedObject[key] = _this.mockFunc(mockedObject, mockedObject[key]);
239            });
240        return mockedObject;
241    }
242}
243
244function ifMockedFunction(f: any) {
245    if (Object.prototype.toString.call(f) != "[object Function]" &&
246        Object.prototype.toString.call(f) != "[object AsyncFunction]") {
247        throw Error("not a function");
248    }
249    if (!f.stub) {
250        throw Error("not a mock function");
251    }
252    return true;
253}
254
255function when(f: any) {
256    if (ifMockedFunction(f)) {
257        return f.stub.bind(f);
258    }
259}
260
261function MockSetup(target: Object, propertyName: string | Symbol, descriptor: TypedPropertyDescriptor<() => void>): void {
262    const aboutToAppearOrigin = target.aboutToAppear;
263    const setup = descriptor.value;
264    target.aboutToAppear = function (...args: any[]) {
265        if (target.__Param) { // copy attributes and params of the original context
266            try {
267                const map = target.__Param as Map<string, unknown>;
268                for (const [key, val] of map) {
269                    this[key] = val; // 'this' refers to context of current function
270                }
271            } catch (e) {
272                throw new Error(`Mock setup param error: ${e}`);
273            }
274        }
275
276        if (setup) { // apply the mock content
277            try {
278                setup.apply(this);
279            } catch (e) {
280                throw new Error(`Mock setup apply error: ${e}`);
281            }
282        }
283
284        if (aboutToAppearOrigin) { // append to aboutToAppear function of the original context
285            aboutToAppearOrigin.apply(this, args);
286        }
287    }
288}
289
290export {
291    MockSetup,
292    MockKit,
293    when
294};