/**
 * Test case for data transmission over HTTP(S)
 *
 *  Copyright (C) 2010-2019 R-T Specialty, LLC.
 *
 *  This file is part of the Liza Data Collection Framework
 *
 *  Liza is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

const dapi = require('../../../').dapi,
  expect = require('chai').expect,
  Class = require('easejs').Class,
  Sut = dapi.http.HttpDataApi,
  dummy_url = 'http://foo',
  dummy_impl = Class.implement(dapi.http.HttpImpl).extend({
    requestData: function (_, __, ___, ____) {},
    setOptions() {},
  })(),
  dummy_sut = Sut(dummy_url, 'GET', dummy_impl);

describe('HttpDataApi', () => {
  it('is a DataApi', () => {
    expect(Class.isA(dapi.DataApi, dummy_sut)).to.be.ok;
  });

  it('permits RFC 2616 HTTP methods', () => {
    const m = ['GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'TRACE'];

    m.forEach(method => {
      expect(() => {
        Sut(dummy_url, method, dummy_impl);
      }).to.not.throw(Error);
    });
  });

  it('does not permit non-RFC-2616 HTTP methods', () => {
    expect(() => {
      Sut(dummy_url, 'FOO', dummy_impl);
    }).to.throw(Error, 'FOO');
  });

  it('rejects non-HttpImpl objects', () => {
    expect(() => {
      Sut(dummy_url, 'GET', {});
    }).to.throw(TypeError, 'HttpImpl');
  });

  describe('.request', () => {
    const impl = Class('StubHttpImpl')
      .implement(dapi.http.HttpImpl)
      .extend({
        provided: [],
        data: '',
        err: null,

        requestData: function (url, method, data, c) {
          this.provided = arguments;
          c(this.err, this.data);
        },

        setOptions() {},
      })();

    /**
     * The actual request is performed by some underling implementation.
     * This additional level of indirection allows the general concept of an
     * "HTTP Data API" to vary from an underyling HTTP protocol
     * implementation; they are separate concerns, although the distinction
     * may seem subtle.
     */
    it('delegates to provided HTTP implementation', () => {
      const method = 'GET',
        data = 'ribbit',
        c = function () {};

      Sut(dummy_url, method, impl).request(data, c);

      const provided = impl.provided;
      expect(provided[0]).to.equal(dummy_url);
      expect(provided[1]).to.equal(method);
      expect(provided[2]).to.equal(data);
      expect(provided[3]).to.equal(c);
    });

    [
      // default is urlencoded
      {
        enctype: '',
        method: 'POST',
        data: {foo: 'bar=baz', '&bar': 'moo%cow'},
        expected:
          'foo=' +
          encodeURIComponent('bar=baz') +
          '&' +
          encodeURIComponent('&bar') +
          '=' +
          encodeURIComponent('moo%cow'),
      },

      // same as above
      {
        enctype: 'application/x-www-form-urlencoded',
        method: 'POST',
        data: {foo: 'bar=baz', '&bar': 'moo%cow'},
        expected:
          'foo=' +
          encodeURIComponent('bar=baz') +
          '&' +
          encodeURIComponent('&bar') +
          '=' +
          encodeURIComponent('moo%cow'),
      },

      // empty string
      {
        enctype: 'application/x-www-form-urlencoded',
        method: 'POST',
        data: {},
        expected: '',
      },

      // json
      {
        enctype: 'application/json',
        method: 'POST',
        data: {foo: 'bar'},
        expected: '{"foo":"bar"}',
      },

      // ignored if GET
      {
        enctype: 'application/json',
        method: 'GET',
        data: {foo: 'bar'},
        expected: 'foo=bar',
      },
    ].forEach(({enctype, method, data, expected}, i) => {
      it(`${method} encodes (${i})`, () => {
        Sut(dummy_url, method, impl, enctype).request(data, _ => {});

        expect(impl.provided[2]).to.equal(expected);
      });
    });

    [
      {
        label: 'urls without brackets are passed through unchanged',
        url: '/unchanged',
        data: {},
        expected: '/unchanged',
      },
      {
        label: 'urls with brackets have fields substituted',
        url: '/this/has/{val}/changed',
        data: {
          val: 'been',
        },
        expected: '/this/has/been/changed',
      },
      {
        label: 'fields will not be substituted if it is not a string',
        url: '/not/{val}/subbed',
        data: {
          val: {
            foo: 'bar',
          },
        },
        expected: '/not/{val}/subbed',
      },
      {
        label: 'fields will not be substituted if data does not have the key',
        url: '/not/{val}/subbed',
        data: {},
        expected: '/not/{val}/subbed',
      },
      {
        label: 'multiple values can be substituted in url',
        url: '/{one}/{two}',
        data: {
          one: 'foo',
          two: 'bar',
        },
        expected: '/foo/bar',
      },
    ].forEach(({label, url, data, expected}) => {
      it(label, () => {
        Sut(url, 'POST', impl, 'application/json').request(data, _ => {});

        expect(impl.provided[0]).to.equal(expected);
      });
    });

    /**
     * Method chaining
     */
    it('returns self', () => {
      const sut = Sut(dummy_url, 'GET', impl),
        ret = sut.request('', () => {});

      expect(ret).to.equal(sut);
    });

    /**
     * String requests are intended to be raw messages, whereas objects are
     * treated as key-value params.
     */
    it('accepts string and object data', () => {
      expect(() => {
        Sut(dummy_url, 'GET', impl)
          .request('', () => {}) // string
          .request({}, () => {}); // object
      }).to.not.throw(Error);
    });

    it('accepts null data, converting to empty string', () => {
      expect(() => {
        Sut(dummy_url, 'GET', impl).request(null, () => {
          expect(impl.provided[2]).to.equal('');
        });
      }).to.not.throw(Error);
    });

    it('rejects all other data types', () => {
      const sut = Sut(dummy_url, 'GET', impl);

      [123, Infinity, undefined, NaN, function () {}].forEach(data => {
        expect(() => {
          sut.request(data, () => {});
        }).to.throw(TypeError);
      });
    });

    it('returns error provided by HTTP implementation', done => {
      impl.err = Error('Test impl error');
      Sut(dummy_url, 'GET', impl).request('', (err, resp) => {
        expect(err).to.equal(impl.err);
        done();
      });
    });

    it('returns response provided by HTTP implementation', done => {
      impl.data = {};
      Sut(dummy_url, 'GET', impl).request('', (err, resp) => {
        expect(resp).to.equal(impl.data);
        done();
      });
    });
  });
});
