/* eslint-env node, mocha */
const assert = require('assert');
const sinon = require('sinon');
const { expect } = require('chai');
const path = require('path');
const fs = require('fs-extra');
const { AuthlessClient, AuthlessLowLevelClient, Cache } = require('../../src');
const { AuthlessRouter } = require('@authless/router');
const {
  AuthlessServiceLinkedin,
  AuthlessServiceLinkedinPerson
} = require('@authless/service-linkedin');

const urls = {
  company: 'https://www.linkedin.com/company/meero/',
  person: {
    'valid:valid': 'https://linkedin.com/in/ACwAAA-6v7cBnv1QyxY58cNfvZ5Ifcjt-ZTurZA',
    'missing:valid': 'https://linkedin.com/in/ACwAAA-6v7cBnv1QyxY58cNfvZ5Ifcjt-ZTurZA.missing.valid',
    'missing:missing': 'https://linkedin.com/in/ACwAAA-6v7cBnv1QyxY58cNfvZ5Ifcjt-ZTurZA.missing.missing',
    'missing:invalid': 'https://linkedin.com/in/ACwAAA-6v7cBnv1QyxY58cNfvZ5Ifcjt-ZTurZA.missing.invalid',
  }
}

describe('authless-client', () => {
  let authlessClient, authlessLowLevelClient, authlessLinkedin, authlessLinkedinPerson;
  let existsFnLow, existsFnHigh, retrieveFnLow, retrieveFnHigh;
  let storeFnLow, storeFnHigh, removeFnLow, removeFnHigh;
  let exists, retrieve, store, remove;
  let fakeAxios = { get: {} };
  let fakeAxiosGets = {};
  before(async () => {
    exists = (url, folder) => fs.exists(
      path.join(__dirname, `./data/${folder}/`, `${encodeURIComponent(url)}.json`));
    retrieve = (url, folder) => fs.readJson(
      path.join(__dirname, `./data/${folder}/`, `${encodeURIComponent(url)}.json`));
    store = (url, data, folder) => fs.writeJson(
      path.join(__dirname, `./data/${folder}/`, `${encodeURIComponent(url)}.json`), data);
    remove = (url, folder) => fs.remove(
      path.join(__dirname, `./data/${folder}/`, `${encodeURIComponent(url)}.json`));

    // existsFn
    existsFnLow = sinon.spy(async (url) => exists(url, 'low'));
    existsFnHigh = sinon.spy(async (url) => exists(url, 'high'));
    // existsFn
    retrieveFnLow = sinon.spy(async (url) => retrieve(url, 'low'));
    retrieveFnHigh = sinon.spy(async (url) => retrieve(url, 'high'));
    // sotreFn
    storeFnLow = sinon.spy(async (url, data) => store(url, data, 'low'));
    storeFnHigh = sinon.spy(async (url, data) => true);
    // removeFn
    removeFnLow = sinon.spy(async (url) => remove(url, 'low'));
    removeFnHigh = sinon.spy(async (url) => true);
    // axios
    fakeAxiosGets.valid = sinon.fake.resolves({ data: await retrieve(urls.person['valid:valid'], 'low') })
    fakeAxiosGets.invalid = sinon.fake.resolves({ data: await retrieve(urls.person['valid:valid'], 'high') })

    // inits
    const cacheLow = new Cache({
      name: 'low',
      existsFn: existsFnLow,
      retrieveFn: retrieveFnLow,
      storeFn: storeFnLow,
      removeFn: removeFnLow
    });
    const cacheHigh = new Cache({
      name: 'high',
      existsFn: existsFnHigh,
      retrieveFn: retrieveFnHigh,
      storeFn: storeFnHigh,
      removeFn: removeFnHigh
    });
    authlessLowLevelClient = new AuthlessLowLevelClient({
      uri: 'http://example.com:4000',
      retries: 1,
      cache: cacheLow,
      axios: fakeAxios
    });
    authlessLinkedin = new AuthlessServiceLinkedin();
    authlessLinkedinPerson = new AuthlessServiceLinkedinPerson();
    authlessClient = new AuthlessClient({
      lowLevelClient: authlessLowLevelClient,
      retries: 1,
      router: new AuthlessRouter([
        ...authlessLinkedin.getRoutes(),
        ...authlessLinkedinPerson.getRoutes(),
      ]),
      cache: cacheHigh
    });
  });

  describe(`source.valid`, () => {
    let subject;
    before(() => {
      fakeAxios.get = fakeAxiosGets.valid;
    });

    beforeEach(() => {
      fakeAxios.get.resetHistory();
      existsFnLow.resetHistory();
      retrieveFnLow.resetHistory();
      storeFnLow.resetHistory();
      removeFnLow.resetHistory();

      existsFnHigh.resetHistory();
      retrieveFnHigh.resetHistory();
      storeFnHigh.resetHistory();
      removeFnHigh.resetHistory();
    });

    context('high.valid & low.valid', () => {
      beforeEach(() => subject = urls.person['valid:valid']);
      it('returns high.valid', async () => {
        const response = await authlessClient.url({u: subject});
        assert.deepEqual(response, await retrieveFnHigh(subject));
      });

      it('calls correct services', async () => {
        await authlessClient.url({u: subject});
        assert.equal(fakeAxios.get.callCount, 0);

        assert.equal(existsFnLow.callCount, 0);
        assert.equal(retrieveFnLow.callCount, 0);
        assert.equal(storeFnLow.callCount, 0);
        assert.equal(removeFnLow.callCount, 0);

        assert.equal(existsFnHigh.callCount, 1);
        assert.equal(retrieveFnHigh.callCount, 1);
        assert.equal(storeFnHigh.callCount, 0);
        assert.equal(removeFnHigh.callCount, 0);
      });
    });

    context('high.missing & low.valid', () => {
      beforeEach(() => subject = urls.person['missing:valid']);
      it('returns high.valid', async () => {
        const response = await authlessClient.url({u: subject});
        assert.deepEqual(response, await retrieveFnHigh(urls.person['valid:valid']));
      });

      it('calls correct services', async () => {
        await authlessClient.url({u: subject});
        assert.equal(fakeAxios.get.callCount, 0);

        assert.equal(existsFnLow.callCount, 1);
        assert.equal(retrieveFnLow.callCount, 1);
        assert.equal(storeFnLow.callCount, 0);
        assert.equal(removeFnLow.callCount, 0);

        assert.equal(existsFnHigh.callCount, 1);
        assert.equal(retrieveFnHigh.callCount, 0);
        assert.equal(storeFnHigh.callCount, 1);
        assert.equal(removeFnHigh.callCount, 0);
      });
    });

    context('high.missing & low.missing', () => {
      beforeEach(async () => {
        subject = urls.person['missing:missing']
        await remove(urls.person['missing:missing'], 'low');
      });
      it('returns high.valid', async () => {
        const response = await authlessClient.url({u: subject});
        assert.deepEqual(response, await retrieveFnHigh(urls.person['valid:valid']));
      });

      it('calls correct services', async () => {
        await authlessClient.url({u: subject});
        assert.equal(fakeAxios.get.callCount, 1);

        assert.equal(existsFnLow.callCount, 1);
        assert.equal(retrieveFnLow.callCount, 0);
        assert.equal(storeFnLow.callCount, 1);
        assert.equal(removeFnLow.callCount, 0);

        assert.equal(existsFnHigh.callCount, 1);
        assert.equal(retrieveFnHigh.callCount, 0);
        assert.equal(storeFnHigh.callCount, 1);
        assert.equal(removeFnHigh.callCount, 0);
      });
    });

    context('high.missing & low.invalid', () => {
      beforeEach(async () => {
        subject = urls.person['missing:invalid']
        const high = await retrieve(urls.person['valid:valid'], 'high');
        await store(urls.person['missing:invalid'], high, 'low');
      });
      it('returns high.valid', async () => {
        const response = await authlessClient.url({u: subject});
        assert.deepEqual(response, await retrieveFnHigh(urls.person['valid:valid']));
      });

      it('calls correct services', async () => {
        await authlessClient.url({u: subject});
        assert.equal(fakeAxios.get.callCount, 1);

        assert.equal(existsFnLow.callCount, 2);
        assert.equal(retrieveFnLow.callCount, 1);
        assert.equal(storeFnLow.callCount, 1);
        assert.equal(removeFnLow.callCount, 1);

        assert.equal(existsFnHigh.callCount, 2);
        assert.equal(retrieveFnHigh.callCount, 0);
        assert.equal(storeFnHigh.callCount, 1);
        assert.equal(removeFnHigh.callCount, 0);
      });
    });
  });

  describe(`source.invalid`, () => {
    let subject;
    before(() => {
      fakeAxios.get = fakeAxiosGets.invalid;
    });

    beforeEach(() => {
      fakeAxios.get.resetHistory();
      existsFnLow.resetHistory();
      retrieveFnLow.resetHistory();
      storeFnLow.resetHistory();
      removeFnLow.resetHistory();

      existsFnHigh.resetHistory();
      retrieveFnHigh.resetHistory();
      storeFnHigh.resetHistory();
      removeFnHigh.resetHistory();
    });

    context('high.valid & low.valid', () => {
      beforeEach(() => subject = urls.person['valid:valid']);
      it('returns high.valid', async () => {
        const response = await authlessClient.url({u: subject});
        assert.deepEqual(response, await retrieveFnHigh(subject));
      });

      it('calls correct services', async () => {
        await authlessClient.url({u: subject});
        assert.equal(fakeAxios.get.callCount, 0);

        assert.equal(existsFnLow.callCount, 0);
        assert.equal(retrieveFnLow.callCount, 0);
        assert.equal(storeFnLow.callCount, 0);
        assert.equal(removeFnLow.callCount, 0);

        assert.equal(existsFnHigh.callCount, 1);
        assert.equal(retrieveFnHigh.callCount, 1);
        assert.equal(storeFnHigh.callCount, 0);
        assert.equal(removeFnHigh.callCount, 0);
      });
    });

    context('high.missing & low.valid', () => {
      beforeEach(() => subject = urls.person['missing:valid']);
      it('returns high.valid', async () => {
        const response = await authlessClient.url({u: subject});
        assert.deepEqual(response, await retrieveFnHigh(urls.person['valid:valid']));
      });

      it('calls correct services', async () => {
        await authlessClient.url({u: subject});
        assert.equal(fakeAxios.get.callCount, 0);

        assert.equal(existsFnLow.callCount, 1);
        assert.equal(retrieveFnLow.callCount, 1);
        assert.equal(storeFnLow.callCount, 0);
        assert.equal(removeFnLow.callCount, 0);

        assert.equal(existsFnHigh.callCount, 1);
        assert.equal(retrieveFnHigh.callCount, 0);
        assert.equal(storeFnHigh.callCount, 1);
        assert.equal(removeFnHigh.callCount, 0);
      });
    });

    context('high.missing & low.missing', () => {
      beforeEach(async () => {
        subject = urls.person['missing:missing']
        await remove(urls.person['missing:missing'], 'low');
      });
      it('fails', async () => {
        await assert.rejects(async () => authlessClient.url({u: subject}));
      });

      it('calls correct services', async () => {
        await assert.rejects(async () => authlessClient.url({u: subject}));
        assert.equal(fakeAxios.get.callCount, 2);

        assert.equal(existsFnLow.callCount, 2);
        assert.equal(retrieveFnLow.callCount, 0);
        assert.equal(storeFnLow.callCount, 2);
        assert.equal(removeFnLow.callCount, 2);

        assert.equal(existsFnHigh.callCount, 2);
        assert.equal(retrieveFnHigh.callCount, 0);
        assert.equal(storeFnHigh.callCount, 0);
        assert.equal(removeFnHigh.callCount, 0);
      });
    });

    context('high.missing & low.invalid', () => {
      beforeEach(async () => {
        subject = urls.person['missing:invalid']
        const high = await retrieve(urls.person['valid:valid'], 'high');
        await store(urls.person['missing:invalid'], high, 'low');
      });
      it('fails', async () => {
        await assert.rejects(async () => authlessClient.url({u: subject}));
      });

      it('calls correct services', async () => {
        await assert.rejects(async () => authlessClient.url({u: subject}));
        assert.equal(fakeAxios.get.callCount, 1);

        assert.equal(existsFnLow.callCount, 2);
        assert.equal(retrieveFnLow.callCount, 1);
        assert.equal(storeFnLow.callCount, 1);
        assert.equal(removeFnLow.callCount, 2);

        assert.equal(existsFnHigh.callCount, 2);
        assert.equal(retrieveFnHigh.callCount, 0);
        assert.equal(storeFnHigh.callCount, 0);
        assert.equal(removeFnHigh.callCount, 0);
      });
    });
  });
});
