/**
 * Tests MongoServerDao
 *
 *  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/>.
 */

'use strict';

import {MongoServerDao as Sut} from '../../../src/server/db/MongoServerDao';
import {Collection, Cursor} from 'mongodb';
import {expect, use as chai_use} from 'chai';
import {
  ServerSideQuote,
  FieldState,
} from '../../../src/server/quote/ServerSideQuote';
import {PositiveInteger} from '../../../src/numeric';
import {ClassificationResult, Program} from '../../../src/program/Program';
import {RateResult} from '../../../src/server/rater/Rater';
import {QuoteDataBucket} from '../../../src/bucket/QuoteDataBucket';
import {QuoteId} from '../../../src/quote/Quote';
import {DataApiResult} from '../../../src/dapi/DataApi';

chai_use(require('chai-as-promised'));

describe('MongoServerDao', () => {
  describe('#saveQuote', () => {
    [
      {
        label:
          'will not unset last updated by user when session data is unavailable',
        username: '',
        expected_set_username: undefined,
      },
      {
        label: 'will set last updated by user when session data is available',
        username: 'foo',
        expected_set_username: ['foo'],
      },
    ].forEach(({label, username, expected_set_username}) => {
      it(label, done => {
        const quote = createStubQuote({});
        quote.getUserName = () => username;

        const sut = new Sut(
          createStubCollection(
            // update
            (_selector: any, data: any) => {
              expect(data.$set['meta.last_updated_by_username']).to.deep.equal(
                expected_set_username
              );
              done();
            }
          ),
          createStubCollection(),
          'test',
          () => <UnixTimestamp>123
        );

        sut.init(() =>
          sut.saveQuote(
            quote,
            () => {},
            () => {}
          )
        );
      });
    });

    describe('with no save data or push data', () => {
      it('saves initial rated (and other fields) individually', done => {
        const expected = 123321;
        const expected_username = 'foo@foo.com';
        const expected_agency_num = 'AGT123';
        const expected_start_date = 246;
        const expected_agent_id = '123';
        const expected_agent_name = 'foo';
        const expected_agent_entity_id = 456;

        const quote = createStubQuote({});

        quote.getRatedDate = () => {
          return <UnixTimestamp>expected;
        };
        quote.getUserName = () => expected_username;
        quote.getAgencyNumber = () => expected_agency_num;
        const sut = new Sut(
          createStubCollection(
            // update
            (_selector: any, data: any) => {
              expect(
                data.$set['meta.liza_timestamp_initial_rated']
              ).to.deep.equal([expected]);

              expect(data.$set['startDate']).to.deep.equal(expected_start_date);

              expect(data.$set['agentId']).to.deep.equal(expected_agent_id);

              expect(data.$set['agentName']).to.deep.equal(expected_agent_name);

              expect(data.$set['agentEntityId']).to.deep.equal(
                expected_agent_entity_id
              );

              expect(
                data.$setOnInsert['meta.created_by_username']
              ).to.deep.equal([expected_username]);

              expect(data.$setOnInsert['meta.retail_agency']).to.deep.equal([
                expected_agency_num,
              ]);

              expect(data.$push).to.equal(undefined);

              done();
            }
          ),
          createStubCollection(),
          'test',
          () => {
            return <UnixTimestamp>123;
          }
        );

        sut.init(() =>
          sut.saveQuote(
            quote,
            () => {},
            () => {}
          )
        );
      });
    });

    describe('bound by username', () => {
      [
        {
          label: 'will set bound_by_username when quote is imported',
          username: 'foo@foo.com',
          expected_bound_username: ['foo@foo.com'],
          is_imported: true,
        },
        {
          label: 'will not set bound_by_username when quote is not imported',
          username: 'foo@foo.com',
          expected_bound_username: undefined,
          is_imported: false,
        },
      ].forEach(({label, username, expected_bound_username, is_imported}) => {
        it(label, done => {
          const quote = createStubQuote({});

          quote.getUserName = () => username;
          quote.isImported = () => is_imported;

          const sut = new Sut(
            createStubCollection(
              // update
              (_selector: any, data: any) => {
                expect(data.$set['meta.bound_by_username']).to.deep.equal(
                  expected_bound_username
                );

                done();
              }
            ),
            createStubCollection(),
            'test',
            () => {
              return <UnixTimestamp>123;
            }
          );

          sut.init(() =>
            sut.saveQuote(
              quote,
              () => {},
              () => {}
            )
          );
        });
      });
    });

    it('non-persistent fields are not saved', done => {
      const bucket_data = {
        foo: ['bar'],
        __internal_user: ['0'],
      };

      const quote = createStubQuote({}, bucket_data);

      const sut = new Sut(
        createStubCollection(
          // update
          (_selector: any, data: any) => {
            expect(data.$set['data.foo']).to.deep.equal(['bar']);
            expect(data.$set['data.__internal_user']).to.be.undefined;

            done();
          }
        ),
        createStubCollection(),
        'test',
        () => {
          return <UnixTimestamp>123;
        }
      );

      sut.init(() =>
        sut.saveQuote(
          quote,
          () => {},
          () => {}
        )
      );
    });

    describe('with force_publish', () => {
      it('forced_reset resets published indicator', done => {
        const expected_ts = <UnixTimestamp>12345;
        const quote = createStubQuote({published: true});
        const sut = new Sut(
          createStubCollection(
            // update
            (_selector: any, data: any) => {
              expect(data.$set['published']).to.deep.equal(false);
              expect(data.$set['publishResetTs']).to.deep.equal(expected_ts);
              done();
            }
          ),
          createStubCollection(),
          'test',
          () => {
            return expected_ts;
          }
        );

        sut.init(() =>
          sut.saveQuote(
            quote,
            () => {},
            () => {},
            undefined,
            {},
            true
          )
        );
      });

      it('not forced will not change published indicator', done => {
        const quote = createStubQuote({published: true});
        const sut = new Sut(
          createStubCollection(
            // update
            (_selector: any, data: any) => {
              expect(data.$set['published']).to.not.exist;
              expect(data.$set['publishResetTs']).to.not.exist;
              done();
            }
          ),
          createStubCollection(),
          'test',
          () => {
            return <UnixTimestamp>123;
          }
        );

        sut.init(() =>
          sut.saveQuote(
            quote,
            () => {},
            () => {},
            undefined,
            {},
            false
          )
        );
      });
    });

    describe('with push data', () => {
      it('adds push data to the collection', done => {
        const push_data = {
          foo: ['bar', 'baz'],
          bar: [{quux: 'quuux'}],
        };

        const quote = createStubQuote({});

        const sut = new Sut(
          createStubCollection(
            // update
            (_selector: any, data: any) => {
              expect(data.$push['foo']).to.deep.equal(push_data.foo);

              expect(data.$push['bar']).to.deep.equal(push_data.bar);

              done();
            }
          ),
          createStubCollection(),
          'test',
          () => {
            return <UnixTimestamp>123;
          }
        );

        sut.init(() =>
          sut.saveQuote(
            quote,
            () => {},
            () => {},
            undefined,
            push_data
          )
        );
      });

      it('skips push data when it is an empty object', done => {
        const push_data = {};

        const quote = createStubQuote({});

        const sut = new Sut(
          createStubCollection(
            // update
            (_selector: any, data: any) => {
              expect(data.$push).to.equal(undefined);

              done();
            }
          ),
          createStubCollection(),
          'test',
          () => {
            return <UnixTimestamp>123;
          }
        );

        sut.init(() =>
          sut.saveQuote(
            quote,
            () => {},
            () => {},
            undefined,
            push_data
          )
        );
      });
    });
  });

  describe('#saveQuoteMetaDirect', () => {
    [
      {
        label: 'Merges existing meta data when new meta is not provided',
        new_meta: undefined,
        existing_meta: {old: ['meta data']},
        expected: {'meta.old': ['meta data']},
      },
      {
        label: 'Merges new meta data if provided',
        new_meta: {foo: ['bar']},
        existing_meta: {this: ['is', 'ignored']},
        expected: {'meta.foo': ['bar']},
      },
      {
        label: 'Merges all indexes of the new meta data',
        new_meta: {foo: ['bar', 'baz', 'qux']},
        existing_meta: {},
        expected: {'meta.foo': ['bar', 'baz', 'qux']},
      },
    ].forEach(({label, new_meta, existing_meta, expected}) => {
      it(label, done => {
        const quote = createStubQuote(existing_meta);

        const sut = new Sut(
          createStubCollection(
            // update
            (_selector: any, data: any) => {
              expect(data.$set).to.deep.equal(expected);
              done();
            }
          ),
          createStubCollection(),
          'test',
          () => <UnixTimestamp>123
        );

        sut.init(() =>
          sut.saveQuoteMetaDirect(quote, new_meta, undefined, undefined)
        );
      });
    });
  });

  describe('#publishQuote', () => {
    [
      {
        label: 'sets quote published',
        publish: true,
        meta_retail_agency: ['AGT23690'],
        expected_liza_access_groups: ['AGT23690'],
      },
      {
        label: 'sets quote not published',
        publish: false,
        meta_retail_agency: ['AGT23690'],
        expected_liza_access_groups: [],
      },
      {
        label: 'set published with blank retail agency',
        publish: true,
        meta_retail_agency: [''],
        expected_liza_access_groups: [''],
      },
      {
        label: 'set published with NO retail_agency',
        publish: true,
        meta_retail_agency: undefined,
        expected_liza_access_groups: undefined,
      },
    ].forEach(
      ({label, publish, meta_retail_agency, expected_liza_access_groups}) => {
        it(label, done => {
          const quote = createStubQuote({retail_agency: meta_retail_agency});

          const published_ts = [<UnixTimestamp>123];

          const sut = new Sut(
            createStubCollection(
              // update
              (_selector: any, data: any) => {
                expect(data.$set['meta.liza_access_groups']).to.deep.equal(
                  expected_liza_access_groups
                );
                expect(
                  data.$set['meta.liza_timestamp_owner_vis_updated']
                ).to.deep.equal(published_ts);

                done();
              }
            ),
            createStubCollection(),
            'test',
            () => <UnixTimestamp>123
          );

          sut.init(() => sut.publishQuote(quote, publish));
        });
      }
    );
  });

  describe('#saveState', () => {
    type TestData = {
      label: string;
      given: FieldState | undefined;
      expected: FieldState | undefined;
    };

    (<TestData[]>[
      {
        label: 'does not set field state if undefined',
        given: undefined,
        expected: undefined,
      },

      {
        label: 'does not set field state if empty',
        given: {},
        expected: undefined,
      },

      {
        label: 'sets field state if non-empty',
        given: {foo: 1},
        expected: {foo: 1},
      },
    ]).forEach(({label, given, expected}) =>
      it(label, done => {
        let update_data: Record<string, any> = {__not: 'updated'};

        const sut = new Sut(
          createStubCollection(
            // update
            (
              _selector: any,
              data: any,
              _options: any,
              callback: NodeCallback<unknown, unknown>
            ) => {
              update_data = data;
              callback(null, null);
            }
          ),
          createStubCollection(),
          'test',
          () => <UnixTimestamp>123
        );

        const quote = createStubQuote({});
        quote.getFieldState = () => given;

        const success = () => {
          expect(update_data['$set'].fieldState).to.deep.equal(expected);

          if (expected === undefined) {
            expect(
              Object.prototype.hasOwnProperty.call(update_data, 'fieldState')
            ).to.be.false;
          }

          done();
        };
        const failure = (e: any) => {
          throw e;
        };

        sut.saveQuoteState(quote, success, failure);
      })
    );
  });
});

function createStubCollection(
  onUpdate: any = () => {},
  result = 1
): Collection {
  const cursor = createMockCursor(result);

  return <Collection>(<unknown>{
    update: onUpdate,
    updateOne: onUpdate,
    insertOne: onUpdate,
    find: () => cursor,
  });
}

function createMockCursor(result = 1): Cursor {
  const cursor = <Cursor>(<unknown>{
    limit: () => cursor,
    project: () => cursor,
    next: (c: any) => {
      c(null, result);
    },
  });

  return cursor;
}

function createStubQuote(
  metadata: Record<string, any>,
  data: Record<string, any> = {}
) {
  const program = <Program>(<unknown>{
    clearNaFields: false,
    naFieldValue: '',
    getId: () => '1',
    ineligibleLockCount: 0,
    cretain: {},
    defaults: {},
    apis: {},
    internal: {},
    autosave: false,
    whens: {},
    groupWhens: {},
    meta: {
      arefs: {},
      fields: {},
      groups: {},
      qdata: {},
      qtypes: {},
    },
    mapis: {},
    rateSteps: [],
    dapi: () => <DataApiResult>{},
    initQuote: () => {},
    getClassifierKnownFields: () => <ClassificationResult>{},
    classify: () => <ClassificationResult>{},
    hasNaField: () => false,
    hasKnownType: () => true,
    getFirstStepId: () => 0,
    groupExclusiveFields: {},
    processNaFields: () => {},
    groupIndexField: {},
  });

  const quote = <ServerSideQuote>(<unknown>{
    getBucket: () =>
      <QuoteDataBucket>{
        getData: () => {
          return data;
        },
      },

    getMetabucket: () =>
      <QuoteDataBucket>{
        getData: () => metadata,
      },

    getId: () => <QuoteId>123,
    getProgramVersion: () => 'Foo',
    getLastPremiumDate: () => <UnixTimestamp>0,
    getStartDate: () => <UnixTimestamp>246,
    getAgentId: () => <UnixTimestamp>123,
    getAgentName: () => 'foo',
    getAgentEntityId: () => <UnixTimestamp>456,
    getUserName: () => 'foo@foo.com',
    getAgencyNumber: () => '',
    getRatedDate: () => <UnixTimestamp>0,
    getExplicitLockReason: () => '',
    getExplicitLockStep: () => <PositiveInteger>1,
    isImported: () => false,
    isBound: () => false,
    getTopVisitedStepId: () => <PositiveInteger>1,
    getTopSavedStepId: () => <PositiveInteger>1,
    setTopSavedStepId: () => quote,
    getMetaUpdatedDate: () => <UnixTimestamp>0,
    setRatedDate: () => quote,
    setRateBucket: () => quote,
    setRatingData: () => quote,
    getRatingData: () => <RateResult>{_unavailable_all: '0'},
    getProgram: () => program,
    setExplicitLock: () => quote,
    getProgramId: () => 'Foo',
    getCurrentStepId: () => 0,
    setCurrentStepId: () => quote,
    setLastPremiumDate: () => quote,
    setRetryAttempts: () => quote,
    getRetryAttempts: () => 1,
    retryAttempted: () => quote,
    setMetadata: () => quote,
    setInitialRatedDate: () => quote,
    getExpirationDate: () => 123,
  });

  return quote;
}
