1/*
2 * Copyright (c) 2022-2024 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';
17import VerificationMode from './VerificationMode';
18import ArgumentMatchers from './ArgumentMatchers';
19
20class MockKit {
21
22    constructor() {
23        this.mFunctions = [];
24        this.stubs = new Map();
25        this.recordCalls = new Map();
26        this.currentSetKey = new Map();
27        this.mockObj = null;
28        this.recordMockedMethod = new Map();
29    }
30
31    init() {
32        this.reset();
33    }
34
35    reset() {
36        this.mFunctions = [];
37        this.stubs = {};
38        this.recordCalls = {};
39        this.currentSetKey = new Map();
40        this.mockObj = null;
41        this.recordMockedMethod = new Map();
42    }
43
44    clearAll() {
45        this.reset();
46        var props = Object.keys(this);
47        for (var i = 0; i < props.length; i++) {
48            delete this[props[i]];
49        }
50
51        var props = Object.getOwnPropertyNames(this);
52        for (var i = 0; i < props.length; i++) {
53            delete this[props[i]];
54        }
55        for (var key in this) {
56            delete this[key];
57        }
58    }
59
60    clear(obj) {
61        if (!obj) {
62            throw Error('Please enter an object to be cleaned');
63        }
64        if (typeof (obj) !== 'object' && typeof (obj) !== 'function') {
65            throw new Error('Not a object or static class');
66        }
67        this.recordMockedMethod.forEach(function (value, key, map) {
68            if (key) {
69                obj[key] = value;
70            }
71        });
72    }
73
74    ignoreMock(obj, method) {
75        if (typeof (obj) !== 'object' && typeof (obj) !== 'function') {
76            throw new Error('Not a object or static class');
77        }
78        if (typeof (method) !== 'function') {
79            throw new Error('Not a function');
80        }
81        let og = this.recordMockedMethod.get(method.propName);
82        if (og) {
83            obj[method.propName] = og;
84            this.recordMockedMethod.set(method.propName, undefined);
85        }
86    }
87
88    extend(dest, source) {
89        dest['stub'] = source['stub'];
90        dest['afterReturn'] = source['afterReturn'];
91        dest['afterReturnNothing'] = source['afterReturnNothing'];
92        dest['afterAction'] = source['afterAction'];
93        dest['afterThrow'] = source['afterThrow'];
94        dest['stubMockedCall'] = source['stubMockedCall'];
95        dest['clear'] = source['clear'];
96        return dest;
97    }
98
99    stubApply(f, params, returnInfo) {
100        let values = this.stubs.get(f);
101        if (!values) {
102            values = new Map();
103        }
104        let key = params[0];
105        if (typeof key == 'undefined') {
106            key = 'anonymous-mock-' + f.propName;
107        }
108        let matcher = new ArgumentMatchers();
109        if (matcher.matcheStubKey(key)) {
110            key = matcher.matcheStubKey(key);
111            if (key) {
112                this.currentSetKey.set(f, key);
113            }
114        }
115        values.set(key, returnInfo);
116        this.stubs.set(f, values);
117    }
118
119    getReturnInfo(f, params) {
120        let values = this.stubs.get(f);
121        if (!values) {
122            return undefined;
123        }
124        let retrunKet = params[0];
125        if (typeof retrunKet == 'undefined') {
126            retrunKet = 'anonymous-mock-' + f.propName;
127        }
128        let stubSetKey = this.currentSetKey.get(f);
129
130        if (stubSetKey && (typeof (retrunKet) !== 'undefined')) {
131            retrunKet = stubSetKey;
132        }
133        let matcher = new ArgumentMatchers();
134        if (matcher.matcheReturnKey(params[0], undefined, stubSetKey) && matcher.matcheReturnKey(params[0], undefined, stubSetKey) !== stubSetKey) {
135            retrunKet = params[0];
136        }
137
138        values.forEach(function (value, key, map) {
139            if (ArgumentMatchers.isRegExp(key) && matcher.matcheReturnKey(params[0], key)) {
140                retrunKet = key;
141            }
142        });
143
144        return values.get(retrunKet);
145    }
146
147    findName(obj, value) {
148        let properties = this.findProperties(obj);
149        let name = null;
150        properties.filter(item => (item !== 'caller' && item !== 'arguments')).forEach(
151            function (va1, idx, array) {
152                if (obj[va1] === value) {
153                    name = va1;
154                }
155            }
156        );
157        return name;
158    }
159
160    isFunctionFromPrototype(f, container, propName) {
161        if (container.constructor !== Object && container.constructor.prototype !== container) {
162            return container.constructor.prototype[propName] === f;
163        }
164        return false;
165    }
166
167    findProperties(obj, ...arg) {
168        function getProperty(newObj) {
169            if (newObj.__proto__ === null) {
170                return [];
171            }
172            let properties = Object.getOwnPropertyNames(newObj);
173            return [...properties, ...getProperty(newObj.__proto__)];
174        }
175        return getProperty(obj);
176    }
177
178    recordMethodCall(originalMethod, args) {
179        Function.prototype.getName = function () {
180            return this.name || this.toString().match(/function\s*([^(]*)\(/)[1];
181        };
182        let name = originalMethod.getName();
183        let arglistString = name + '(' + Array.from(args).toString() + ')';
184        let records = this.recordCalls.get(arglistString);
185        if (!records) {
186            records = 0;
187        }
188        records++;
189        this.recordCalls.set(arglistString, records);
190    }
191
192    mockFunc(originalObject, originalMethod) {
193        let tmp = this;
194        this.originalMethod = originalMethod;
195        let f = function () {
196            let args = arguments;
197            let action = tmp.getReturnInfo(f, args);
198            if (originalMethod) {
199                tmp.recordMethodCall(originalMethod, args);
200            }
201            if (action) {
202                return action.apply(this, args);
203            }
204        };
205
206        f.container = null || originalObject;
207        f.original = originalMethod || null;
208
209        if (originalObject && originalMethod) {
210            if (typeof (originalMethod) !== 'function') {
211                throw new Error('Not a function');
212            }
213            var name = this.findName(originalObject, originalMethod);
214            originalObject[name] = f;
215            this.recordMockedMethod.set(name, originalMethod);
216            f.propName = name;
217            f.originalFromPrototype = this.isFunctionFromPrototype(f.original, originalObject, f.propName);
218        }
219        f.mocker = this;
220        this.mFunctions.push(f);
221        this.extend(f, new ExtendInterface(this));
222        return f;
223    }
224
225    verify(methodName, argsArray) {
226        if (!methodName) {
227            throw Error('not a function name');
228        }
229        let a = this.recordCalls.get(methodName + '(' + argsArray.toString() + ')');
230        return new VerificationMode(a ? a : 0);
231    }
232
233    mockObject(object) {
234        if (!object || typeof object === 'string') {
235            throw Error(`this ${object} cannot be mocked`);
236        }
237        const _this = this;
238        let mockedObject = {};
239        let keys = Reflect.ownKeys(object);
240        keys.filter(key => (typeof Reflect.get(object, key)) === 'function')
241            .forEach(key => {
242                mockedObject[key] = object[key];
243                mockedObject[key] = _this.mockFunc(mockedObject, mockedObject[key]);
244            });
245        return mockedObject;
246    }
247}
248
249function ifMockedFunction(f) {
250    if (Object.prototype.toString.call(f) !== '[object Function]' &&
251        Object.prototype.toString.call(f) !== '[object AsyncFunction]') {
252        throw Error('not a function');
253    }
254    if (!f.stub) {
255        throw Error('not a mock function');
256    }
257    return true;
258}
259
260function when(f) {
261    if (!ifMockedFunction(f)) {
262        throw Error('not a mock function');
263    }
264    return f.stub.bind(f);
265}
266
267export {MockKit, when};