/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */

import {patchMethod, patchProperty, patchPrototype, zoneSymbol} from '../../lib/common/utils';

describe('utils', function() {
  describe('patchMethod', () => {
    it('should patch target where the method is defined', () => {
      let args: any[]|undefined;
      let self: any;
      class Type {
        method(..._args: any[]) {
          args = _args;
          self = this;
          return 'OK';
        }
      }
      const method = Type.prototype.method;
      let delegateMethod: Function;
      let delegateSymbol: string;

      const instance = new Type();
      expect(patchMethod(instance, 'method', (delegate: Function, symbol: string, name: string) => {
        expect(name).toEqual('method');
        delegateMethod = delegate;
        delegateSymbol = symbol;
        return function(self, args) {
          return delegate.apply(self, ['patch', args[0]]);
        };
      })).toBe(delegateMethod!);

      expect(instance.method('a0')).toEqual('OK');
      expect(args).toEqual(['patch', 'a0']);
      expect(self).toBe(instance);
      expect(delegateMethod!).toBe(method);
      expect(delegateSymbol!).toEqual(zoneSymbol('method'));
      expect((Type.prototype as any)[delegateSymbol!]).toBe(method);
    });

    it('should not double patch', () => {
      const Type = function() {};
      const method = Type.prototype.method = function() {};
      patchMethod(Type.prototype, 'method', (delegate) => {
        return function(self, args: any[]) {
          return delegate.apply(self, ['patch', ...args]);
        };
      });
      const pMethod = Type.prototype.method;
      expect(pMethod).not.toBe(method);
      patchMethod(Type.prototype, 'method', (delegate) => {
        return function(self, args) {
          return delegate.apply(self, ['patch', ...args]);
        };
      });
      expect(pMethod).toBe(Type.prototype.method);
    });

    it('should not patch property which is not configurable', () => {
      const TestType = function() {};
      const originalDefineProperty = (Object as any)[zoneSymbol('defineProperty')];
      if (originalDefineProperty) {
        originalDefineProperty(
            TestType.prototype, 'nonConfigurableProperty',
            {configurable: false, writable: true, value: 'test'});
      } else {
        Object.defineProperty(
            TestType.prototype, 'nonConfigurableProperty',
            {configurable: false, writable: true, value: 'test'});
      }
      patchProperty(TestType.prototype, 'nonConfigurableProperty');
      const desc = Object.getOwnPropertyDescriptor(TestType.prototype, 'nonConfigurableProperty');
      expect(desc!.writable).toBeTruthy();
      expect(!desc!.get).toBeTruthy();
    });
  });

  describe('patchPrototype', () => {
    it('non configurable property desc should be patched', () => {
      'use strict';
      const TestFunction: any = function() {};
      const log: string[] = [];
      Object.defineProperties(TestFunction.prototype, {
        'property1': {
          value: function Property1(callback: Function) {
            Zone.root.run(callback);
          },
          writable: true,
          configurable: true,
          enumerable: true
        },
        'property2': {
          value: function Property2(callback: Function) {
            Zone.root.run(callback);
          },
          writable: true,
          configurable: false,
          enumerable: true
        }
      });

      const zone = Zone.current.fork({name: 'patch'});

      zone.run(() => {
        const instance = new TestFunction();
        instance.property1(() => {
          log.push('property1' + Zone.current.name);
        });
        instance.property2(() => {
          log.push('property2' + Zone.current.name);
        });
      });
      expect(log).toEqual(['property1<root>', 'property2<root>']);
      log.length = 0;

      patchPrototype(TestFunction.prototype, ['property1', 'property2']);

      zone.run(() => {
        const instance = new TestFunction();
        instance.property1(() => {
          log.push('property1' + Zone.current.name);
        });
        instance.property2(() => {
          log.push('property2' + Zone.current.name);
        });
      });
      expect(log).toEqual(['property1patch', 'property2patch']);
    });

    it('non writable property desc should not be patched', () => {
      'use strict';
      const TestFunction: any = function() {};
      const log: string[] = [];
      Object.defineProperties(TestFunction.prototype, {
        'property1': {
          value: function Property1(callback: Function) {
            Zone.root.run(callback);
          },
          writable: true,
          configurable: true,
          enumerable: true
        },
        'property2': {
          value: function Property2(callback: Function) {
            Zone.root.run(callback);
          },
          writable: false,
          configurable: true,
          enumerable: true
        }
      });

      const zone = Zone.current.fork({name: 'patch'});

      zone.run(() => {
        const instance = new TestFunction();
        instance.property1(() => {
          log.push('property1' + Zone.current.name);
        });
        instance.property2(() => {
          log.push('property2' + Zone.current.name);
        });
      });
      expect(log).toEqual(['property1<root>', 'property2<root>']);
      log.length = 0;

      patchPrototype(TestFunction.prototype, ['property1', 'property2']);

      zone.run(() => {
        const instance = new TestFunction();
        instance.property1(() => {
          log.push('property1' + Zone.current.name);
        });
        instance.property2(() => {
          log.push('property2' + Zone.current.name);
        });
      });
      expect(log).toEqual(['property1patch', 'property2<root>']);
    });

    it('readonly property desc should not be patched', () => {
      'use strict';
      const TestFunction: any = function() {};
      const log: string[] = [];
      Object.defineProperties(TestFunction.prototype, {
        'property1': {
          get: function() {
            if (!this._property1) {
              this._property1 = function Property2(callback: Function) {
                Zone.root.run(callback);
              };
            }
            return this._property1;
          },
          set: function(func: Function) {
            this._property1 = func;
          },
          configurable: true,
          enumerable: true
        },
        'property2': {
          get: function() {
            return function Property2(callback: Function) {
              Zone.root.run(callback);
            };
          },
          configurable: true,
          enumerable: true
        }
      });

      const zone = Zone.current.fork({name: 'patch'});

      zone.run(() => {
        const instance = new TestFunction();
        instance.property1(() => {
          log.push('property1' + Zone.current.name);
        });
        instance.property2(() => {
          log.push('property2' + Zone.current.name);
        });
      });
      expect(log).toEqual(['property1<root>', 'property2<root>']);
      log.length = 0;

      patchPrototype(TestFunction.prototype, ['property1', 'property2']);

      zone.run(() => {
        const instance = new TestFunction();
        instance.property1(() => {
          log.push('property1' + Zone.current.name);
        });
        instance.property2(() => {
          log.push('property2' + Zone.current.name);
        });
      });
      expect(log).toEqual(['property1patch', 'property2<root>']);
    });

    it('non writable method should not be patched', () => {
      'use strict';
      const TestFunction: any = function() {};
      const log: string[] = [];
      Object.defineProperties(TestFunction.prototype, {
        'property2': {
          value: function Property2(callback: Function) {
            Zone.root.run(callback);
          },
          writable: false,
          configurable: true,
          enumerable: true
        }
      });

      const zone = Zone.current.fork({name: 'patch'});

      zone.run(() => {
        const instance = new TestFunction();
        instance.property2(() => {
          log.push('property2' + Zone.current.name);
        });
      });
      expect(log).toEqual(['property2<root>']);
      log.length = 0;

      patchMethod(
          TestFunction.prototype, 'property2',
          function(delegate: Function, delegateName: string, name: string) {
            return function(self: any, args: any) {
              log.push('patched property2');
            };
          });

      zone.run(() => {
        const instance = new TestFunction();
        instance.property2(() => {
          log.push('property2' + Zone.current.name);
        });
      });
      expect(log).toEqual(['property2<root>']);
    });

    it('readonly method should not be patched', () => {
      'use strict';
      const TestFunction: any = function() {};
      const log: string[] = [];
      Object.defineProperties(TestFunction.prototype, {
        'property2': {
          get: function() {
            return function Property2(callback: Function) {
              Zone.root.run(callback);
            };
          },
          configurable: true,
          enumerable: true
        }
      });

      const zone = Zone.current.fork({name: 'patch'});

      zone.run(() => {
        const instance = new TestFunction();
        instance.property2(() => {
          log.push('property2' + Zone.current.name);
        });
      });
      expect(log).toEqual(['property2<root>']);
      log.length = 0;

      patchMethod(
          TestFunction.prototype, 'property2',
          function(delegate: Function, delegateName: string, name: string) {
            return function(self: any, args: any) {
              log.push('patched property2');
            };
          });

      zone.run(() => {
        const instance = new TestFunction();
        instance.property2(() => {
          log.push('property2' + Zone.current.name);
        });
      });
      expect(log).toEqual(['property2<root>']);
    });
  });
});
