import { createSpyFromClass } from '../create-spy-from-class';
import {
  FakeClass,
  FakeAbstractClass,
  FakeGetterSetterClass,
} from './fake-classes-to-test';
import { Spy } from '../jasmine-auto-spies.types';
import { errorHandler } from '@hirez_io/auto-spies-core';

let fakeClassSpy: Spy<FakeClass>;
const FAKE_VALUE = 'FAKE SYNC VALUE';
let actualResult: any;
let fakeArgs: any[];
const WRONG_VALUE = 'WRONG VALUE';
let throwArgumentsErrorSpyFunction: jasmine.Spy;

function verifyArgumentsErrorWasThrown({
  actualArgs,
  expectedMethodName,
}: {
  actualArgs: any[];
  expectedMethodName: string;
}) {
  expect(throwArgumentsErrorSpyFunction).toHaveBeenCalledWith(
    actualArgs,
    expectedMethodName
  );
}

describe('createSpyFromClass', () => {
  Given(() => {
    actualResult = null;
    fakeArgs = [];

    throwArgumentsErrorSpyFunction = spyOn(errorHandler, 'throwArgumentsError');
    fakeClassSpy = createSpyFromClass(FakeClass);
  });

  describe('GIVEN a synchronous method is being configured', () => {
    Given(() => {
      fakeClassSpy.getSyncValue.and.returnValue(FAKE_VALUE);
    });

    When(() => {
      actualResult = fakeClassSpy.getSyncValue();
    });

    Then('return the correct value', () => {
      expect(actualResult).toBe(FAKE_VALUE);
    });
  });

  describe('GIVEN a synchronous method is being manually configured', () => {
    Given(() => {
      fakeClassSpy = createSpyFromClass(FakeClass, ['arrowMethod']);
      fakeClassSpy.arrowMethod.and.returnValue(FAKE_VALUE);
    });

    When(() => {
      actualResult = fakeClassSpy.arrowMethod();
    });

    Then('return the correct value', () => {
      expect(actualResult).toBe(FAKE_VALUE);
    });
  });

  describe('GIVEN a synchronous method is being manually configured using the config object', () => {
    Given(() => {
      fakeClassSpy = createSpyFromClass(FakeClass, {
        methodsToSpyOn: ['arrowMethod'],
      });
      fakeClassSpy.arrowMethod.and.returnValue(FAKE_VALUE);
    });

    When(() => {
      actualResult = fakeClassSpy.arrowMethod();
    });

    Then('return the correct value', () => {
      expect(actualResult).toBe(FAKE_VALUE);
    });
  });

  describe(`GIVEN a synchronous method is being configured with specific parameters
            and returns a non undefined value`, () => {
    Given(() => {
      fakeArgs = [1, { a: 2 }];
      fakeClassSpy.getSyncValue.calledWith(...fakeArgs).returnValue(FAKE_VALUE);
    });

    describe('WHEN it is called with the expected parameters', () => {
      When(() => {
        actualResult = fakeClassSpy.getSyncValue(...fakeArgs);
      });

      Then('return the correct value', () => {
        expect(actualResult).toBe(FAKE_VALUE);
      });
    });

    describe(`GIVEN it is configured differently for the same input arguments 
              WHEN it is called with the expected parameters`, () => {
      Given(() => {
        fakeClassSpy.getSyncValue.calledWith(...fakeArgs).returnValue('FAKE VALUE 2');
      });

      When(() => {
        actualResult = fakeClassSpy.getSyncValue(...fakeArgs);
      });

      Then('return the correct value', () => {
        expect(actualResult).toBe('FAKE VALUE 2');
      });
    });

    describe('WHEN it is called with the wrong parameters', () => {
      When(() => {
        actualResult = fakeClassSpy.getSyncValue(WRONG_VALUE);
      });

      Then('return undefined', () => {
        expect(actualResult).toBeUndefined();
      });
    });

    describe(`GIVEN another calledWith is configured
                WHEN method is called twice`, () => {
      let actualResult2: any;
      let fakeArgs2: any[];
      let fakeValue2: any;
      Given(() => {
        actualResult2 = undefined;
        fakeValue2 = 'FAKE VALUE 2';
        fakeArgs2 = [3, 4];
        fakeClassSpy.getSyncValue.calledWith(...fakeArgs2).returnValue(fakeValue2);
      });

      When(() => {
        actualResult = fakeClassSpy.getSyncValue(...fakeArgs);
        actualResult2 = fakeClassSpy.getSyncValue(...fakeArgs2);
      });

      Then('return the correct value for each call', () => {
        expect(actualResult).toBe(FAKE_VALUE);
        expect(actualResult2).toBe(fakeValue2);
      });
    });

    describe('WHEN called with the wrong parameters', () => {
      When(() => {
        actualResult = fakeClassSpy.getSyncValue(WRONG_VALUE);
      });

      Then('do NOT throw an error', () => {
        expect(throwArgumentsErrorSpyFunction).not.toHaveBeenCalled();
      });
    });
  });

  describe(`GIVEN a synchronous method is being configured with specific parameters
            and returns a null value
            WHEN called with the matching parameters`, () => {
    Given(() => {
      fakeArgs = [1, { a: 2 }];
      fakeClassSpy.getNullableSyncValue.calledWith(...fakeArgs).returnValue(null);
    });

    When(() => {
      actualResult = fakeClassSpy.getNullableSyncValue(...fakeArgs);
    });

    Then('return null', () => {
      expect(actualResult).toBeNull();
    });
  });

  describe(`GIVEN a synchronous method is configured to throw on mismatch
            WHEN called with the wrong parameters`, () => {
    let expectedMethodName: string;
    Given(() => {
      expectedMethodName = 'getSyncValue';
      fakeArgs = [1, { a: 2 }];
      fakeClassSpy.getSyncValue.mustBeCalledWith(...fakeArgs).returnValue(FAKE_VALUE);
    });
    When(() => {
      actualResult = fakeClassSpy.getSyncValue(WRONG_VALUE);
    });

    Then('throw an error', () => {
      verifyArgumentsErrorWasThrown({
        expectedMethodName,
        actualArgs: [WRONG_VALUE],
      });
    });
  });

  describe(`GIVEN a synchronous method is configured with mustBeCalledWith twice with different values`, () => {
    let fakeArgs2: any[];
    Given(() => {
      fakeArgs = [1, { a: 2 }];
      fakeArgs2 = [1, { a: 3 }];
      fakeClassSpy.getSyncValue.mustBeCalledWith(...fakeArgs).returnValue(FAKE_VALUE);
      fakeClassSpy.getSyncValue.mustBeCalledWith(...fakeArgs2).returnValue(FAKE_VALUE);
    });

    describe(`WHEN called twice with the right parameters`, () => {
      When(() => {
        actualResult = fakeClassSpy.getSyncValue(...fakeArgs);
        actualResult = fakeClassSpy.getSyncValue(...fakeArgs2);
      });

      Then('do NOT throw an error', () => {
        expect(throwArgumentsErrorSpyFunction).not.toHaveBeenCalled();
      });
    });

    describe(`WHEN called second time with the wrong parameters`, () => {
      let expectedMethodName: string;
      Given(() => {
        expectedMethodName = 'getSyncValue';
      });

      When(() => {
        actualResult = fakeClassSpy.getSyncValue(...fakeArgs);
        actualResult = fakeClassSpy.getSyncValue(WRONG_VALUE);
      });

      Then('throw an error with the wrong value', () => {
        verifyArgumentsErrorWasThrown({
          expectedMethodName,
          actualArgs: [WRONG_VALUE],
        });
      });
    });
  });

  describe('GIVEN an abstract class', () => {
    let abstractClassSpy: Spy<FakeAbstractClass>;

    describe('GIVEN sync method is configured', () => {
      Given(() => {
        abstractClassSpy = createSpyFromClass(FakeAbstractClass as any);
        abstractClassSpy.getSyncValue.and.returnValue('FAKE');
      });

      When(() => {
        actualResult = abstractClassSpy.getSyncValue();
      });
      Then('return the correct value', () => {
        expect(actualResult).toBe('FAKE');
      });
    });

    describe('GIVEN abstract method is configured', () => {
      Given(() => {
        abstractClassSpy = createSpyFromClass(FakeAbstractClass as any, [
          'needToImplementThis',
        ]);
        abstractClassSpy.needToImplementThis.and.returnValue('FAKE');
      });

      When(() => {
        actualResult = abstractClassSpy.needToImplementThis();
      });
      Then('return the correct value', () => {
        expect(actualResult).toBe('FAKE');
      });
    });
  });

  describe('getters and setters', () => {
    let fakeGetterSetterClass: Spy<FakeGetterSetterClass>;

    describe('GIVEN spying on class with a property that has both getter and setter', () => {
      Given(() => {
        fakeGetterSetterClass = createSpyFromClass(FakeGetterSetterClass, {
          gettersToSpyOn: ['myProp'],
          settersToSpyOn: ['myProp'],
        });
      });
      describe('GIVEN getter spy configured with fake return value', () => {
        Given(() => {
          fakeGetterSetterClass.accessorSpies.getters.myProp.and.returnValue(FAKE_VALUE);
        });
        Then('return the fake value', () => {
          expect(fakeGetterSetterClass.myProp).toBe(FAKE_VALUE);
        });
      });

      describe('GIVEN setter spy configured WHEN setting the var', () => {
        When(() => {
          fakeGetterSetterClass.myProp = '2';
        });
        Then('allow spying on setter', () => {
          expect(fakeGetterSetterClass.accessorSpies.setters.myProp).toHaveBeenCalled();
        });
      });
    });

    describe(`GIVEN spying on class with a property that has only a getter
              and getter spy configured with fake return value`, () => {
      Given(() => {
        fakeGetterSetterClass = createSpyFromClass(FakeGetterSetterClass, {
          gettersToSpyOn: ['anotherGetter'],
        });

        fakeGetterSetterClass.accessorSpies.getters.anotherGetter.and.returnValue(222);
      });

      Then('return the fake value', () => {
        expect(fakeGetterSetterClass.anotherGetter).toBe(222);
      });
    });

    describe(`GIVEN spying on class with a property that has only a setter
              WHEN variable is set`, () => {
      Given(() => {
        fakeGetterSetterClass = createSpyFromClass(FakeGetterSetterClass, {
          settersToSpyOn: ['mySetter'],
        });
      });
      When(() => {
        fakeGetterSetterClass.mySetter = 2222;
      });
      Then('allow spying on setter', () => {
        expect(fakeGetterSetterClass.accessorSpies.setters.mySetter).toHaveBeenCalledWith(
          2222
        );
      });
    });
  });
});
