/**
 * Token state management test
 *
 *  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 Affero 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 Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

import {
  TokenData,
  TokenQueryResult,
  TokenStatus,
} from '../../../src/server/token/TokenDao';

import {MongoTokenDao as Sut} from '../../../src/server/token/MongoTokenDao';
import {Cursor, Collection} from 'mongodb';
import {
  TokenId,
  TokenNamespace,
  TokenState,
} from '../../../src/server/token/Token';

import {DocumentId} from '../../../src/document/Document';
import {UnknownTokenError} from '../../../src/server/token/UnknownTokenError';
import {hasContext} from '../../../src/error/ContextError';

import {expect, use as chai_use} from 'chai';
chai_use(require('chai-as-promised'));

describe('server.token.TokenDao', () => {
  describe('#updateToken', () => {
    const field = 'foo_field';
    const did = <DocumentId>12345;
    const ns = <TokenNamespace>'namespace';
    const tok_id = <TokenId>'tok123';
    const tok_type = TokenState.DONE;
    const data = 'some data';
    const timestamp = <UnixTimestamp>12345;

    const root = field + '.' + ns;

    const last_tok_id = <TokenId>'last-tok';

    const last: TokenStatus = {
      type: TokenState.DEAD,
      timestamp: <UnixTimestamp>4567,
      data: 'last token',
    };

    const prev: TokenStatus = {
      type: TokenState.ACTIVE,
      timestamp: <UnixTimestamp>11111,
      data: 'prev status',
    };

    (<{label: string; given: TokenQueryResult; expected: TokenData}[]>[
      {
        label: 'updates token and returns previous data',

        given: {
          [field]: {
            [ns]: {
              last: last_tok_id,
              lastState: {
                [prev.type]: last_tok_id,
              },
              lastStatus: {
                type: last.type,
                timestamp: last.timestamp,
                data: last.data,
              },
              [tok_id]: {
                status: {
                  type: prev.type,
                  timestamp: prev.timestamp,
                  data: prev.data,
                },
              },
            },
          },
        },
        expected: {
          id: tok_id,
          status: {
            type: tok_type,
            timestamp: timestamp,
            data: data,
          },
          prev_state: {
            [prev.type]: last_tok_id,
          },
          prev_status: prev,
          prev_last: {
            id: last_tok_id,
            status: last,
            prev_status: null,
            prev_last: null,
            prev_state: {},
          },
        },
      },

      {
        label: 'returns null for prev status if missing data',

        given: {
          [field]: {
            [ns]: {
              last: last_tok_id,
              lastStatus: {
                type: last.type,
                timestamp: last.timestamp,
                data: last.data,
              },
            },
          },
        },
        expected: {
          id: tok_id,
          status: {
            type: tok_type,
            timestamp: timestamp,
            data: data,
          },
          prev_status: null,
          prev_state: {},
          prev_last: {
            id: last_tok_id,
            status: last,
            prev_status: null,
            prev_last: null,
            prev_state: {},
          },
        },
      },

      {
        label: 'returns null for missing namespace data',

        given: {
          [field]: {
            [ns]: {},
          },
        },
        expected: {
          id: tok_id,
          status: {
            type: tok_type,
            timestamp: timestamp,
            data: data,
          },
          prev_status: null,
          prev_state: {},
          prev_last: null,
        },
      },

      {
        label: 'returns null for missing namespace',

        given: {
          [field]: {},
        },
        expected: {
          id: tok_id,
          status: {
            type: tok_type,
            timestamp: timestamp,
            data: data,
          },
          prev_status: null,
          prev_state: {},
          prev_last: null,
        },
      },

      {
        label: 'returns null for missing root field',

        given: {},
        expected: {
          id: tok_id,
          status: {
            type: tok_type,
            timestamp: timestamp,
            data: data,
          },
          prev_status: null,
          prev_state: {},
          prev_last: null,
        },
      },
    ]).forEach(({given, expected, label}) =>
      it(label, () => {
        const coll: Collection = <Collection>(<unknown>{
          findOneAndUpdate(
            selector: any,
            given_data: any,
            options: any,
            callback: any
          ) {
            const expected_entry: TokenStatus = {
              type: tok_type,
              timestamp: timestamp,
              data: data,
            };

            expect(selector.id).to.equal(did);

            expect(given_data).to.deep.equal({
              $set: {
                [`${root}.last`]: tok_id,
                [`${root}.lastState.${tok_type}`]: tok_id,
                [`${root}.lastStatus`]: expected_entry,
                [`${root}.${tok_id}.status`]: expected_entry,
              },
              $push: {
                [`${root}.${tok_id}.statusLog`]: expected_entry,
              },
            });

            expect(options).to.deep.equal({
              upsert: true,
              projection: {
                [`${root}.last`]: 1,
                [`${root}.lastState`]: 1,
                [`${root}.lastStatus`]: 1,
                [`${root}.${tok_id}.status`]: 1,
              },
            });

            callback(null, {value: given});
          },

          update() {},
          findOne() {},
          find() {},
          createIndex() {},
          insert() {},
        });

        return expect(
          new Sut(coll, field, () => timestamp).updateToken(
            did,
            ns,
            tok_id,
            tok_type,
            data
          )
        ).to.eventually.deep.equal(expected);
      })
    );

    it('proxies error to callback', () => {
      const expected_error = Error('expected error');

      const coll: Collection = <Collection>(<unknown>{
        findOneAndUpdate(_: any, __: any, ___: any, callback: any) {
          callback(expected_error, {});
        },

        update() {},
        findOne() {},
        find() {},
        createIndex() {},
        insert() {},
      });

      return expect(
        new Sut(coll, 'foo', () => <UnixTimestamp>0).updateToken(
          <DocumentId>0,
          <TokenNamespace>'ns',
          <TokenId>'id',
          TokenState.DONE,
          null
        )
      ).to.eventually.be.rejectedWith(expected_error);
    });
  });

  describe('#getToken', () => {
    const field = 'get_field';
    const did = <DocumentId>12345;
    const ns = <TokenNamespace>'get_ns';

    const expected_status: TokenStatus = {
      type: TokenState.ACTIVE,
      timestamp: <UnixTimestamp>0,
      data: '',
    };

    const last_tok_id = <TokenId>'last-tok';

    const last: TokenStatus = {
      type: TokenState.DEAD,
      timestamp: <UnixTimestamp>4567,
      data: 'last token',
    };

    (<[string, TokenId, TokenQueryResult, TokenData | null, any, any][]>[
      [
        'retrieves token by id',
        <TokenId>'tok123',
        {
          [field]: {
            [ns]: {
              last: last_tok_id,
              lastState: {
                [TokenState.ACTIVE]: last_tok_id,
                [TokenState.DONE]: last_tok_id,
              },
              lastStatus: last,

              tok123: {
                status: expected_status,
                statusLog: [expected_status],
              },
            },
          },
        },
        {
          id: <TokenId>'tok123',
          status: expected_status,
          prev_status: expected_status,
          prev_state: {
            [TokenState.ACTIVE]: last_tok_id,
            [TokenState.DONE]: last_tok_id,
          },
          prev_last: {
            id: last_tok_id,
            status: last,
            prev_status: null,
            prev_last: null,
            prev_state: {},
          },
        },
        null,
        null,
      ],

      [
        'rejects for namespace if token is not found',
        <TokenId>'tok123',
        {
          [field]: {
            [ns]: {
              last: last_tok_id,
              lastStatus: last,

              // just to make sure we don't grab another tok
              othertok: {
                status: expected_status,
                statusLog: [expected_status],
              },
            },
          },
        },
        null,
        `${ns}.tok123`,
        {
          doc_id: did,
          quote_id: did,
          ns: ns,
          token_id: 'tok123',
        },
      ],

      [
        'rejects if namespace is not found',
        <TokenId>'tok123',
        {
          [field]: {},
        },
        null,
        ns,
        {
          doc_id: did,
          quote_id: did,
          ns: ns,
        },
      ],

      [
        'returns last modified token given no token id',
        <TokenId>'',
        {
          [field]: {
            [ns]: {
              last: last_tok_id,
              lastState: {
                [TokenState.DEAD]: last_tok_id,
              },
              lastStatus: last,

              [last_tok_id]: {
                status: expected_status,
                statusLog: [expected_status],
              },
            },
          },
        },
        {
          id: last_tok_id,
          status: last,
          prev_status: last,
          prev_state: {
            [TokenState.DEAD]: last_tok_id,
          },
          prev_last: {
            id: last_tok_id,
            status: last,
            prev_status: null,
            prev_last: null,
            prev_state: {},
          },
        },
        null,
        null,
      ],

      [
        'rejects unknown last modified token given no token id',
        <TokenId>'',
        {
          [field]: {
            [ns]: {},
          },
        },
        null,
        ns,
        {
          doc_id: did,
          quote_id: did,
          ns: ns,
        },
      ],

      [
        'rejects unknown namespace token given no token id',
        <TokenId>'',
        {
          [field]: {},
        },
        null,
        ns,
        {
          doc_id: did,
          quote_id: did,
          ns: ns,
        },
      ],
    ]).forEach(([label, tok_id, dbresult, expected, fmsg, fcontext]) =>
      it(label, () => {
        const cursor: Cursor = <Cursor>(<unknown>{
          project: () => this,
          next: (cb: any) => {
            cb(undefined, dbresult);
          },
        });
        const coll: Collection = <Collection>(<unknown>{
          find: (selector: any) => {
            expect(selector).to.deep.equal({id: did});

            return {
              project: (fields: any) => {
                const expected_fields = {
                  [`${field}.${ns}.last`]: 1,
                  [`${field}.${ns}.lastState`]: 1,
                  [`${field}.${ns}.lastStatus`]: 1,
                };

                if (tok_id) {
                  expected_fields[`${field}.${ns}.${tok_id}`] = 1;
                }

                expect(fields).to.deep.equal(expected_fields);

                return {
                  limit: () => cursor,
                };
              },
            };
          },
          update() {},
          findAndModify() {},
          createIndex() {},
          insert() {},
        });

        const result = new Sut(coll, field, () => <UnixTimestamp>0).getToken(
          did,
          ns,
          tok_id
        );

        return fmsg !== null
          ? Promise.all([
              expect(result).to.eventually.be.rejectedWith(
                UnknownTokenError,
                fmsg
              ),
              expect(result).to.eventually.be.rejectedWith(
                UnknownTokenError,
                '' + did
              ),
              result.catch(e => {
                if (!hasContext(e)) {
                  // TS will soon have type assertions and
                  // then this conditional and return can be
                  // removed
                  return expect.fail();
                }

                return expect(e.context).to.deep.equal(fcontext);
              }),
            ])
          : expect(result).to.eventually.deep.equal(expected);
      })
    );

    it('proxies error to callback', () => {
      const expected_error = Error('expected error');

      const coll: Collection = <Collection>(<unknown>{
        find(_: any) {
          throw expected_error;
        },

        update() {},
        findAndModify() {},
        createIndex() {},
        insert() {},
      });

      return expect(
        new Sut(coll, 'foo', () => <UnixTimestamp>0).getToken(
          <DocumentId>0,
          <TokenNamespace>'ns',
          <TokenId>'id'
        )
      ).to.eventually.be.rejectedWith(expected_error);
    });
  });
});
