/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';

const assert = require('assert');
const helper = require('../test-helper');
const types = require('../../lib/types');
const RequestLogger = require('../../lib/tracker').RequestLogger;
const ExecutionOptions = require('../../lib/execution-options').ExecutionOptions;

describe('RequestLogger', () => {
  describe('#onSuccess()', () => {
    context('when request is slow', () => {
      const logger = new RequestLogger({ slowThreshold: 200 });
      const loggerShortMessage = new RequestLogger({
        slowThreshold: 200, messageMaxQueryLength: 20, messageMaxParameterValueLength: 5 });

      let message;
      const handler = (m) => message = m;

      logger.emitter.on('slow', handler);
      loggerShortMessage.emitter.on('slow', handler);

      beforeEach(() => message = null);

      it('should include the query and parameters in the log message', () => {
        const query = 'INSERT EXAMPLE';
        logger.onSuccess({ address: '::1' }, query, ['a', true, 1.01], getExecutionOptions(false), 10, 20, [ 1, 2034125 ]);
        helper.assertContains(message, query, true);
        helper.assertContains(message, 'Slow request, took 1002 ms');
        helper.assertContains(message, '[a,true,1.01]');
        helper.assertContains(message, 'not prepared');
      });

      it('should stringify object parameters in the log message', () => {
        const query = 'UPDATE EXAMPLE';
        logger.onSuccess({ address: '::1' }, query, { a: 1, b: true, c: 1.01 }, getExecutionOptions(), 10, 20, [ 1, 0 ]);
        helper.assertContains(message, query, true);
        helper.assertContains(message, 'Slow request, took 1000 ms');
        helper.assertContains(message, '[a:1,b:true,c:1.01]');
        helper.assertContains(message, 'not prepared');
      });

      it('should stringify individual object values', () => {
        const query = 'INSERT SAMPLE (k1, k2) VALUES (?, ?)';
        const params = [ types.Uuid.random(), types.InetAddress.fromString('10.10.10.1') ];
        logger.onSuccess({ address: '::1' }, query, params, getExecutionOptions(true), 1, 2, [ 2, 0 ]);
        helper.assertContains(message, `INSERT SAMPLE (k1, k2) VALUES (?, ?) [${params.join(',')}]`);
      });

      it('should include size information and round to KB', () => {
        logger.onSuccess({ address: '::1' }, 'Q', [], getExecutionOptions(), 12700, 231, [ 1, 0 ]);
        helper.assertContains(message, 'request size 12 KB / response size 231 bytes');
      });

      it('should include batch information and queries', () => {
        const queries = [ { query: 'Q1' }, { query: 'Q2', params: [ 'a', 'b' ]}];
        logger.onSuccess({ address: '::1' }, queries, null, getExecutionOptions(), 80, 231, [ 1, 0 ]);
        helper.assertContains(message, 'request size 80 bytes / response size 231 bytes');
        helper.assertContains(message, 'BATCH w/ 2 not prepared queries (Q1 [],Q2 [a,b])');
      });

      it('should include batch information and truncate queries info', () => {
        const queries = [ { query: 'INSERT1' }, { query: 'INSERT2' }, { query: 'INSERT3', params: [ 'a', 'b' ]}];
        loggerShortMessage.onSuccess({ address: '::1' }, queries, null, getExecutionOptions(), 1, 1, [ 1, 0 ]);
        helper.assertContains(message, 'BATCH w/ 3 not prepared queries (INSERT1 [],INSERT2 [...],...)');
      });

      it('should truncate query in the message when query is too long', () => {
        const query = 'INSERT EXECUTE A B C this_should_not_be_included';
        loggerShortMessage.onSuccess({ address: '::1' }, query, ['a', true], getExecutionOptions(true), 1, 2, [ 2, 0 ]);
        helper.assertContains(message, 'Slow request, took 2000 ms');
        helper.assertContains(message, 'INSERT EXECUTE A B C [...]');
        assert.strictEqual(message.indexOf('this_should_not_be_included'), -1);
      });

      it('should truncate parameters in the message when parameters are too long', () => {
        const query = 'INSERT';
        const params = ['abcdefghijk', 1];
        loggerShortMessage.onSuccess({ address: '::1' }, query, params, getExecutionOptions(true), 1, 2, [ 2, 0 ]);
        helper.assertContains(message, 'Slow request, took 2000 ms');
        helper.assertContains(message, 'INSERT [abcde,1]');
      });

      it('should truncate parameters in the message when query and parameters are too long', () => {
        const query = 'INSERT SAMPLE';
        const params = ['abcde', true, 1];
        loggerShortMessage.onSuccess({ address: '::1' }, query, params, getExecutionOptions(true), 1, 2, [ 2, 0 ]);
        helper.assertContains(message, 'Slow request, took 2000 ms');
        helper.assertContains(message, 'INSERT SAMPLE [abcde,...]');
      });
    });

    context('when request is large', () => {
      const logger = new RequestLogger({ requestSizeThreshold: 500 });
      let message;
      logger.emitter.on('large', m => message = m);

      beforeEach(() => message = null);

      it('should include the query and parameters in the log message', () => {
        const query = 'INSERT EXAMPLE';
        logger.onSuccess({ address: '::1' }, query, ['a', true, 1.01], getExecutionOptions(false), 1000, 20, [ 0, 201 ]);
        helper.assertContains(message, query, true);
        helper.assertContains(message, 'Request exceeded length');
        helper.assertContains(message, 'INSERT EXAMPLE [a,true,1.01]');
        helper.assertContains(message, 'not prepared');
      });

      it('should not include query preparation info when has been prepared', () => {
        const query = 'UPDATE EXAMPLE';
        logger.onSuccess({ address: '::1' }, query, [], getExecutionOptions(true), 1000, 20, [ 0, 201 ]);
        helper.assertContains(message, query, true);
        helper.assertContains(message, 'Request exceeded length');
        assert.strictEqual(message.indexOf('prepared'), -1);
      });
    });

    context('when request is normal', () => {

      const logger = new RequestLogger({ slowThreshold: 200, requestSizeThreshold: 500 });

      let message;
      const otherMessages = [];
      const handler = (m) => otherMessages.push(m);

      logger.emitter.on('normal', m => message = m);
      logger.emitter.on('slow', handler)
        .on('large', handler)
        .on('failure', handler);

      beforeEach(() => {
        message = null;
        otherMessages.length = 0;
      });

      it('should not log normal requests by default', () => {
        logger.onSuccess({ address: '::1' }, 'QUERY', [], getExecutionOptions(), 1, 1, [ 0, 1 ]);
        // Disabled initially
        assert.strictEqual(message, null);
        assert.strictEqual(otherMessages.length, 0);
      });

      it('should log normal requests when logNormalRequests is enabled', () => {
        logger.logNormalRequests = true;
        const query = 'SELECT EXAMPLE';
        logger.onSuccess({ address: '::1' }, query, [], getExecutionOptions(), 1, 1, [ 0, 1 ]);
        assert.deepEqual(otherMessages, []);
        helper.assertContains(message, query, true);
        helper.assertContains(message, 'Request completed normally');
        helper.assertContains(message, 'SELECT EXAMPLE []');
        helper.assertContains(message, 'not prepared');
      });
    });
  });

  describe('#onError()', () => {
    const logger = new RequestLogger({ requestSizeThreshold: 500, logErroredRequests: true });
    let largeMessage;
    let errorMessage;
    logger.emitter.on('large', m => largeMessage = m);
    logger.emitter.on('failure', m => errorMessage = m);

    beforeEach(() => {
      largeMessage = null;
      errorMessage = null;
      logger.logErroredRequests = true;
    });

    context('when request is large', () => {
      it('should include stack trace when large request failed', () => {
        const err = new Error('Sample error');
        logger.onError({ address: '::1' }, 'Q', [1, 2, 'a'], getExecutionOptions(), 27891, err, [ 1, 0 ]);
        assert.strictEqual(errorMessage, null);
        helper.assertContains(largeMessage, 'Request exceeded length and execution failed');
        helper.assertContains(largeMessage, 'request size 27 KB');
        helper.assertContains(largeMessage, '[1,2,a]');
        helper.assertContains(largeMessage, err.stack.substr(0, 200));
      });
    });

    context('when logErroredRequests is enabled initially', () => {
      it('should include stack trace when large request failed', () => {
        const err = new Error('Sample error');
        logger.onError({ address: '::1' }, 'Q', [1, 2, 'a'], getExecutionOptions(), 80, err, [ 1, 0 ]);
        assert.strictEqual(largeMessage, null);
        helper.assertContains(errorMessage, 'Request execution failed');
        helper.assertContains(errorMessage, err.stack.substr(0, 200));
      });

      it('should allow to be disabled using logErroredRequests property', () => {
        const err = new Error('Sample error');
        logger.onError({ address: '::1' }, 'Q', [1, 2, 'a'], getExecutionOptions(), 80, err, [ 1, 0 ]);
        assert.strictEqual(typeof errorMessage, 'string');

        // Disable it
        logger.logErroredRequests = false;
        errorMessage = null;
        logger.onError({ address: '::1' }, 'Q', [1, 2, 'a'], getExecutionOptions(), 80, err, [ 1, 0 ]);
        assert.strictEqual(errorMessage, null);
      });
    });
  });
});

function getExecutionOptions(prepare) {
  const result = ExecutionOptions.empty();
  result.isPrepared = () => prepare;
  return result;
}