// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC 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('node:assert')
const sinon = require('sinon')
const logging = require('selenium-webdriver/lib/logging')

describe('logging', function () {
  let mgr, root, clock

  beforeEach(function setUp() {
    mgr = new logging.LogManager()
    root = mgr.getLogger('')

    clock = sinon.useFakeTimers()
  })

  afterEach(function tearDown() {
    clock.restore()
  })

  describe('LogManager', function () {
    describe('getLogger()', function () {
      it('handles falsey input', function () {
        assert.strictEqual(root, mgr.getLogger())
        assert.strictEqual(root, mgr.getLogger(''))
        assert.strictEqual(root, mgr.getLogger(null))
        assert.strictEqual(root, mgr.getLogger(0))
      })

      it('creates parent loggers', function () {
        let logger = mgr.getLogger('foo.bar.baz')
        assert.strictEqual(logger.parent_, mgr.getLogger('foo.bar'))

        logger = logger.parent_
        assert.strictEqual(logger.parent_, mgr.getLogger('foo'))

        logger = logger.parent_
        assert.strictEqual(logger.parent_, mgr.getLogger(''))

        assert.strictEqual(logger.parent_.parent_, null)
      })
    })
  })

  describe('Logger', function () {
    describe('getEffectiveLevel()', function () {
      it('defaults to OFF', function () {
        assert.strictEqual(root.getLevel(), logging.Level.OFF)
        assert.strictEqual(root.getEffectiveLevel(), logging.Level.OFF)

        root.setLevel(null)
        assert.strictEqual(root.getLevel(), null)
        assert.strictEqual(root.getEffectiveLevel(), logging.Level.OFF)
      })

      it('uses own level if set', function () {
        let logger = mgr.getLogger('foo.bar.baz')
        assert.strictEqual(logger.getLevel(), null)
        assert.strictEqual(logger.getEffectiveLevel(), logging.Level.OFF)

        logger.setLevel(logging.Level.INFO)
        assert.strictEqual(logger.getLevel(), logging.Level.INFO)
        assert.strictEqual(logger.getEffectiveLevel(), logging.Level.INFO)
      })

      it('uses level from set on nearest parent', function () {
        let ancestor = mgr.getLogger('foo')
        ancestor.setLevel(logging.Level.SEVERE)

        let logger = mgr.getLogger('foo.bar.baz')
        assert.strictEqual(logger.getLevel(), null)
        assert.strictEqual(logger.getEffectiveLevel(), logging.Level.SEVERE)
      })
    })

    describe('isLoggable()', function () {
      it("compares level against logger's effective level", function () {
        const log1 = mgr.getLogger('foo')
        log1.setLevel(logging.Level.WARNING)

        const log2 = mgr.getLogger('foo.bar.baz')

        assert(!log2.isLoggable(logging.Level.FINEST))
        assert(!log2.isLoggable(logging.Level.INFO))
        assert(log2.isLoggable(logging.Level.WARNING))
        assert(log2.isLoggable(logging.Level.SEVERE))

        log2.setLevel(logging.Level.INFO)

        assert(!log2.isLoggable(logging.Level.FINEST))
        assert(log2.isLoggable(logging.Level.INFO))
        assert(log2.isLoggable(logging.Level.WARNING))
        assert(log2.isLoggable(logging.Level.SEVERE))

        log2.setLevel(logging.Level.ALL)

        assert(log2.isLoggable(logging.Level.FINEST))
        assert(log2.isLoggable(logging.Level.INFO))
        assert(log2.isLoggable(logging.Level.WARNING))
        assert(log2.isLoggable(logging.Level.SEVERE))
      })

      it('Level.OFF is never loggable', function () {
        function test(level) {
          root.setLevel(level)
          assert(!root.isLoggable(logging.Level.OFF), 'OFF should not be loggable at ' + level)
        }

        test(logging.Level.ALL)
        test(logging.Level.INFO)
        test(logging.Level.OFF)
      })
    })

    describe('log()', function () {
      it('does not invoke loggable if message is not loggable', function () {
        const log = mgr.getLogger('foo')
        log.setLevel(logging.Level.OFF)

        let callback = sinon.spy()
        log.addHandler(callback)
        root.addHandler(callback)

        assert(!callback.called)
      })

      it('invokes handlers for each parent logger', function () {
        const cb1 = sinon.spy()
        const cb2 = sinon.spy()
        const cb3 = sinon.spy()

        const log1 = mgr.getLogger('foo')
        const log2 = mgr.getLogger('foo.bar')
        const log3 = mgr.getLogger('foo.bar.baz')
        const log4 = mgr.getLogger('foo.bar.baz.quot')

        log1.addHandler(cb1)
        log1.setLevel(logging.Level.INFO)

        log2.addHandler(cb2)
        log2.setLevel(logging.Level.WARNING)

        log3.addHandler(cb3)
        log3.setLevel(logging.Level.FINER)

        clock.tick(123456)

        log4.finest('this is the finest message')
        log4.finer('this is a finer message')
        log4.info('this is an info message')
        log4.warning('this is a warning message')
        log4.severe('this is a severe message')

        assert.strictEqual(4, cb1.callCount)
        assert.strictEqual(4, cb2.callCount)
        assert.strictEqual(4, cb3.callCount)

        const entry1 = new logging.Entry(logging.Level.FINER, '[foo.bar.baz.quot] this is a finer message', 123456)
        const entry2 = new logging.Entry(logging.Level.INFO, '[foo.bar.baz.quot] this is an info message', 123456)
        const entry3 = new logging.Entry(logging.Level.WARNING, '[foo.bar.baz.quot] this is a warning message', 123456)
        const entry4 = new logging.Entry(logging.Level.SEVERE, '[foo.bar.baz.quot] this is a severe message', 123456)

        check(cb1.getCall(0).args[0], entry1)
        check(cb1.getCall(1).args[0], entry2)
        check(cb1.getCall(2).args[0], entry3)
        check(cb1.getCall(3).args[0], entry4)

        check(cb2.getCall(0).args[0], entry1)
        check(cb2.getCall(1).args[0], entry2)
        check(cb2.getCall(2).args[0], entry3)
        check(cb2.getCall(3).args[0], entry4)

        check(cb3.getCall(0).args[0], entry1)
        check(cb3.getCall(1).args[0], entry2)
        check(cb3.getCall(2).args[0], entry3)
        check(cb3.getCall(3).args[0], entry4)

        function check(entry, expected) {
          assert.strictEqual(entry.level, expected.level, 'wrong level')
          assert.strictEqual(entry.message, expected.message, 'wrong message')
          assert.strictEqual(entry.timestamp, expected.timestamp, 'wrong time')
        }
      })

      it('does not invoke removed handler', function () {
        root.setLevel(logging.Level.INFO)
        const cb = sinon.spy()

        root.addHandler(cb)
        root.info('hi')
        assert.strictEqual(1, cb.callCount)

        assert(root.removeHandler(cb))
        root.info('bye')
        assert.strictEqual(1, cb.callCount)

        assert(!root.removeHandler(cb))
      })
    })
  })

  describe('getLevel()', function () {
    it('converts named levels', function () {
      assert.strictEqual(logging.Level.DEBUG, logging.getLevel('DEBUG'))
      assert.strictEqual(logging.Level.ALL, logging.getLevel('FAKE'))
    })

    it('converts numeric levels', function () {
      assert.strictEqual(logging.Level.DEBUG, logging.getLevel(logging.Level.DEBUG.value))
    })

    it('normalizes numeric levels', function () {
      assert.strictEqual(logging.Level.OFF, logging.getLevel(logging.Level.OFF.value * 2))

      let diff = logging.Level.SEVERE.value - logging.Level.WARNING.value
      assert.strictEqual(logging.Level.WARNING, logging.getLevel(logging.Level.WARNING.value + diff * 0.5))

      assert.strictEqual(logging.Level.ALL, logging.getLevel(0))
      assert.strictEqual(logging.Level.ALL, logging.getLevel(-1))
    })
  })

  describe('Preferences', function () {
    it('can be converted to JSON', function () {
      let prefs = new logging.Preferences()
      assert.strictEqual('{}', JSON.stringify(prefs))

      prefs.setLevel('foo', logging.Level.DEBUG)
      assert.strictEqual('{"foo":"DEBUG"}', JSON.stringify(prefs))

      prefs.setLevel(logging.Type.BROWSER, logging.Level.FINE)
      assert.strictEqual('{"foo":"DEBUG","browser":"FINE"}', JSON.stringify(prefs))
    })
  })
})
