'use strict';

import SchemaManager from 'lib/utils/SchemaManager';
import { BaseComponent } from 'lib/components/base';

describe('Redoc components', () => {
  describe('BaseComponent', () => {
    let schemaMgr;
    let component;

    beforeAll(() => {
      schemaMgr = new SchemaManager();
      schemaMgr._schema = {tags: []};
    });

    beforeEach(() => {
      component = new BaseComponent(schemaMgr);
    });

    it('should set instance properties', () => {
      component.schemaMgr.should.be.equal(schemaMgr);
      //component.schema.should.be.of.type('object');
      expect(component.componentSchema).toBeNull();
    });

    it('should set componentSchema based on pointer on ngOnInit', () => {
      component.pointer = '/tags';
      component.ngOnInit();
      component.componentSchema.should.be.deepEqual(schemaMgr._schema.tags);
    });

    it('should call prepareModel and init virtual methods after init', () => {
      sinon.spy(component, 'prepareModel');
      sinon.spy(component, 'init');
      component.ngOnInit();
      component.prepareModel.calledOnce.should.be.true;
      component.init.calledOnce.should.be.true;
    });

    describe('dereference', () => {
      beforeAll((done) => {
        schemaMgr.load('/tests/schemas/base-component-dereference.json').then(
          () => done()
        );
      });

      describe('simple dereference', () => {
        let paramWithRef;
        beforeAll(() => {
          component.pointer = '/paths/test1/get';
          component.ngOnInit();
          component.dereference();
          paramWithRef = component.componentSchema.parameters[0];
        });

        it('should not contain $ref property', () => {
          expect(paramWithRef.$ref).toBeUndefined();
        });

        it('should inject Title if not exist based on reference', () => {
          paramWithRef.title.should.be.equal('Simple');
        });

        it('should inject pointer', () => {
          paramWithRef._pointer.should.be.equal('#/definitions/Simple');
        });

        it('should insert correct definition instead of reference', () => {
          delete paramWithRef.title;
          delete paramWithRef._pointer;
          paramWithRef.should.be.deepEqual(schemaMgr.schema.definitions.Simple);
        });
      });

      describe('nested dereference', () => {
        let paramWithRef;
        beforeAll(() => {
          component.pointer = '/paths/test2/get';
          component.ngOnInit();
          component.dereference();
          paramWithRef = component.componentSchema.parameters[0];
        });

        it('should not touch title if exist', () => {
          paramWithRef.title.should.be.equal('NesteTitle');
        });

        it('should resolve nested schema', () => {
          expect(paramWithRef.properties.subschema.$ref).toBeUndefined();
          paramWithRef._pointer.should.be.equal('#/definitions/Nested');
          paramWithRef.properties.subschema._pointer.should.be.equal('#/definitions/Simple');
          paramWithRef.properties.subschema.type.should.be.equal('object');
        });
      });

      describe('array schema dereference', () => {
        let paramWithRef;
        beforeAll(() => {
          component.pointer = '/paths/test3/get';
          component.ngOnInit();
          component.dereference();
          paramWithRef = component.componentSchema.parameters[0];
        });

        it('should resolve array schema', () => {
          expect(paramWithRef.$ref).toBeUndefined();
          expect(paramWithRef.items.schema.$ref).toBeUndefined();
          paramWithRef.type.should.be.equal('array');
          paramWithRef._pointer.should.be.equal('#/definitions/ArrayOfSimple');
          paramWithRef.items.schema._pointer.should.be.equal('#/definitions/Simple');
          paramWithRef.items.schema.type.should.be.equal('object');
        });
      });

      describe('circular dereference', () => {
        let paramWithRef;
        beforeAll(() => {
          component.pointer = '/paths/test4/get';
          component.ngOnInit();
          component.dereference();
          paramWithRef = component.componentSchema.parameters[0];
        });

        it('should resolve circular schema', () => {
          expect(paramWithRef.$ref).toBeUndefined();
          expect(paramWithRef.items.schema.$ref).toBeUndefined();
          paramWithRef.type.should.be.equal('array');
          paramWithRef._pointer.should.be.equal('#/definitions/Circular');
          expect(paramWithRef.items.schema._pointer).toBeUndefined();
          paramWithRef.items.schema.title.should.be.equal('Circular');
        });
      });

      describe('$ref with other fields on the same level', () => {
        let paramWithRef;
        beforeAll(() => {
          component.pointer = '/paths/test5/get';
          component.ngOnInit();
          component.dereference();
          paramWithRef = component.componentSchema.parameters[0];
        });

        it('should skip other fields', () => {
          expect(paramWithRef.$ref).toBeUndefined();
          expect(paramWithRef.title).toBeDefined();
          paramWithRef.title.should.be.equal('Simple');
        });

        it('should preserve description field', () => {
          expect(paramWithRef.$ref).toBeUndefined();
          expect(paramWithRef.description).toBeDefined();
          paramWithRef.description.should.be.equal('test');
        });
      });
    });

    describe('mergeAllOf', () => {
      beforeAll((done) => {
        schemaMgr.load('tests/schemas/base-component-joinallof.json').then(() => done());
      });

      describe('Simple allOf merge', () => {
        let joined;
        beforeAll(() => {
          component.pointer = '/definitions/SimpleAllOf';
          component.ngOnInit();
          component.dereference();
          BaseComponent.joinAllOf(component.componentSchema);
          joined = component.componentSchema;
        });

        it('should remove $allOf field', () => {
          expect(joined.allOf).toBeNull();
        });

        it('should set type object', () => {
          joined.type.should.be.equal('object');
        });

        it('should merge properties', () => {
          Object.keys(joined.properties).length.should.be.equal(3);
          Object.keys(joined.properties).should.be.deepEqual(['prop1', 'prop2', 'prop3']);
        });

        it('should merge required', () => {
          joined.required.length.should.be.equal(2);
          joined.required.should.be.deepEqual(['prop1', 'prop3']);
        });
      });

      describe('AllOf with refrence', () => {
        let joined;
        beforeAll(() => {
          component.pointer = '/definitions/AllOfWithRef';
          component.ngOnInit();
          component.dereference();
          BaseComponent.joinAllOf(component.componentSchema);
          joined = component.componentSchema;
        });

        it('should remove $allOf field', () => {
          expect(joined.allOf).toBeNull();
        });

        it('should set type object', () => {
          joined.type.should.be.equal('object');
        });

        it('should merge properties', () => {
          Object.keys(joined.properties).length.should.be.equal(2);
          Object.keys(joined.properties).should.be.deepEqual(['id', 'prop3']);
        });

        it('should merge required', () => {
          joined.required.length.should.be.equal(2);
          joined.required.should.be.deepEqual(['id', 'prop3']);
        });
      });

      describe('AllOf with other properties on the allOf level', () => {
        let joined;
        beforeAll(() => {
          component.pointer = '/definitions/AllOfWithOther';
          component.ngOnInit();
          component.dereference();
          BaseComponent.joinAllOf(component.componentSchema);
          joined = component.componentSchema;
        });

        it('should remove $allOf field', () => {
          expect(joined.allOf).toBeNull();
        });

        it('should set type object', () => {
          joined.type.should.be.equal('object');
        });

        it('should merge properties', () => {
          Object.keys(joined.properties).length.should.be.equal(1);
          Object.keys(joined.properties).should.be.deepEqual(['id']);
        });

        it('should merge required', () => {
          joined.required.length.should.be.equal(1);
          joined.required.should.be.deepEqual(['id']);
        });

        it('should preserve parent properties', () => {
          joined.description.should.be.equal('Test');
          joined.readOnly.should.be.equal(true);
        });
      });

      describe('allOf edgecases', () => {
        it('should merge properties and required when defined on allOf level', () => {
          component.pointer = '/definitions/PropertiesOnAllOfLevel';
          component.ngOnInit();
          component.dereference();
          (() => BaseComponent.joinAllOf(component.componentSchema)).should.not.throw();
          let joined = component.componentSchema;
          Object.keys(joined.properties).length.should.be.equal(3);
        });

        it('should throw when merging schemas with different types', () => {
          component.pointer = '/definitions/BadAllOf1';
          component.ngOnInit();
          component.dereference();
          (() => BaseComponent.joinAllOf(component.componentSchema)).should.throw();
        });

        it('should handle nested allOF', () => {
          component.pointer = '/definitions/NestedAllOf';
          component.ngOnInit();
          component.dereference();
          (() => BaseComponent.joinAllOf(component.componentSchema)).should.not.throw();
          let joined = component.componentSchema;
          Object.keys(joined.properties).length.should.be.equal(4);
          Object.keys(joined.properties).should.be.deepEqual(['prop1', 'prop2', 'prop3', 'prop4']);
          joined.required.should.be.deepEqual(['prop1', 'prop3']);
        });
      });

      xdescribe('Merge array allOf', () => {
      });
    });
  });
});
