/*
 * Copyright 2021 Juan Cruz Viotti
 *
 * Licensed 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.
 */

import tap from 'tap'
import * as fc from 'fast-check'

import {
  BOUNDED_MULTIPLE_8BITS_ENUM_FIXED as ENCODE_BOUNDED_MULTIPLE_8BITS_ENUM_FIXED,
  FLOOR_MULTIPLE_ENUM_VARINT as ENCODE_FLOOR_MULTIPLE_ENUM_VARINT,
  ROOF_MULTIPLE_MIRROR_ENUM_VARINT as ENCODE_ROOF_MULTIPLE_MIRROR_ENUM_VARINT,
  ARBITRARY_MULTIPLE_ZIGZAG_VARINT as ENCODE_ARBITRARY_MULTIPLE_ZIGZAG_VARINT
} from '../../lib/encoder/integer/encode'

import {
  IntegerResult,
  BOUNDED_MULTIPLE_8BITS_ENUM_FIXED as DECODE_BOUNDED_MULTIPLE_8BITS_ENUM_FIXED,
  FLOOR_MULTIPLE_ENUM_VARINT as DECODE_FLOOR_MULTIPLE_ENUM_VARINT,
  ROOF_MULTIPLE_MIRROR_ENUM_VARINT as DECODE_ROOF_MULTIPLE_MIRROR_ENUM_VARINT,
  ARBITRARY_MULTIPLE_ZIGZAG_VARINT as DECODE_ARBITRARY_MULTIPLE_ZIGZAG_VARINT
} from '../../lib/encoder/integer/decode'

import {
  UINT8_MAX
} from '../../lib/utils/limits'

import {
  ResizableBuffer,
  EncodingContext,
  getDefaultEncodingContext
} from '../../lib/encoder'

tap.test('BOUNDED_MULTIPLE_8BITS_ENUM_FIXED', (test) => {
  const arbitrary = fc.integer().chain((minimum: number) => {
    return fc.tuple(
      fc.nat(10),
      fc.constant(minimum),
      fc.integer(minimum, minimum + UINT8_MAX),
      fc.integer(minimum, minimum + UINT8_MAX))
  })

  fc.assert(fc.property(arbitrary, ([ offset, minimum, maximum, value ]): boolean => {
    fc.pre(value <= maximum)

    const context: EncodingContext = getDefaultEncodingContext()
    const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(offset + 1))
    const bytesWritten: number =
      ENCODE_BOUNDED_MULTIPLE_8BITS_ENUM_FIXED(buffer, offset, value, {
        minimum, maximum, multiplier: 1
      }, context)
    const result: IntegerResult =
      DECODE_BOUNDED_MULTIPLE_8BITS_ENUM_FIXED(buffer, offset, {
        minimum, maximum, multiplier: 1
      })
    return bytesWritten === 1 && result.bytes === bytesWritten && result.value === value
  }), {
    verbose: false
  })

  test.end()
})

tap.test('BOUNDED_MULTIPLE_8BITS_ENUM_FIXED', (test) => {
  const arbitrary = fc.integer().chain((minimum: number) => {
    return fc.integer(minimum, minimum + UINT8_MAX).chain((maximum: number) => {
      return fc.tuple(
        fc.nat(10),
        fc.constant(minimum),
        fc.constant(maximum),
        fc.integer(minimum, maximum),
        fc.integer(minimum, maximum)
      )
    })
  })

  fc.assert(fc.property(arbitrary, ([ offset, minimum, maximum, value, multiplier ]): boolean => {
    fc.pre(value % multiplier === 0)
    const context: EncodingContext = getDefaultEncodingContext()
    const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(offset + 1))
    const bytesWritten: number =
      ENCODE_BOUNDED_MULTIPLE_8BITS_ENUM_FIXED(
        buffer, offset, value, {
          minimum, maximum, multiplier
        }, context)
    const result: IntegerResult =
      DECODE_BOUNDED_MULTIPLE_8BITS_ENUM_FIXED(
        buffer, offset, {
          minimum, maximum, multiplier
        })
    return bytesWritten > 0 && result.bytes === bytesWritten && result.value === value
  }), {
    verbose: false
  })

  test.end()
})

tap.test('FLOOR_MULTIPLE_ENUM_VARINT: should encode 696667952522107300000', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(60))
  const value: number = 696667952522107300000
  const bytesWritten: number =
    ENCODE_FLOOR_MULTIPLE_ENUM_VARINT(buffer, 0, value, {
      minimum: 0,
      multiplier: 1
    }, context)
  const result: IntegerResult =
    DECODE_FLOOR_MULTIPLE_ENUM_VARINT(buffer, 0, {
      minimum: 0,
      multiplier: 1
    })

  test.is(bytesWritten, result.bytes)
  test.is(result.value, value)
  test.end()
})

tap.test('FLOOR_MULTIPLE_ENUM_VARINT', (test) => {
  fc.assert(fc.property(fc.nat(10), fc.integer(), fc.integer(), (
    offset: number, value: number, minimum: number
  ): boolean => {
    fc.pre(value >= minimum)
    const context: EncodingContext = getDefaultEncodingContext()
    const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(offset + 8))
    const bytesWritten: number =
      ENCODE_FLOOR_MULTIPLE_ENUM_VARINT(buffer, offset, value, {
        minimum,
        multiplier: 1
      }, context)
    const result: IntegerResult =
      DECODE_FLOOR_MULTIPLE_ENUM_VARINT(buffer, offset, {
        minimum,
        multiplier: 1
      })
    return bytesWritten > 0 && result.bytes === bytesWritten && result.value === value
  }), {
    verbose: false
  })

  test.end()
})

tap.test('FLOOR_MULTIPLE_ENUM_VARINT', (test) => {
  const arbitrary = fc.integer().chain((minimum: number) => {
    return fc.tuple(
      fc.nat(10),
      fc.constant(minimum),
      fc.integer({
        min: minimum
      }),
      fc.integer({
        min: minimum
      })
    )
  })

  fc.assert(fc.property(arbitrary, ([ offset, minimum, value, multiplier ]): boolean => {
    fc.pre(value % multiplier === 0)
    const context: EncodingContext = getDefaultEncodingContext()
    const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(offset + 8))
    const bytesWritten: number =
      ENCODE_FLOOR_MULTIPLE_ENUM_VARINT(buffer, offset, value, {
        minimum, multiplier
      }, context)
    const result: IntegerResult =
      DECODE_FLOOR_MULTIPLE_ENUM_VARINT(buffer, offset, {
        minimum, multiplier
      })
    return bytesWritten > 0 && result.bytes === bytesWritten && result.value === value
  }), {
    verbose: false
  })

  test.end()
})

tap.test('ROOF_MULTIPLE_MIRROR_ENUM_VARINT', (test) => {
  fc.assert(fc.property(fc.nat(10), fc.integer(), fc.integer(), (
    offset: number, value: number, maximum: number
  ): boolean => {
    fc.pre(value <= maximum)
    const context: EncodingContext = getDefaultEncodingContext()
    const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(offset + 8))
    const bytesWritten: number =
      ENCODE_ROOF_MULTIPLE_MIRROR_ENUM_VARINT(buffer, offset, value, {
        maximum,
        multiplier: 1
      }, context)
    const result: IntegerResult =
      DECODE_ROOF_MULTIPLE_MIRROR_ENUM_VARINT(buffer, offset, {
        maximum,
        multiplier: 1
      })
    return bytesWritten > 0 && result.bytes === bytesWritten && result.value === value
  }), {
    verbose: false
  })

  test.end()
})

tap.test('ROOF_MULTIPLE_MIRROR_ENUM_VARINT', (test) => {
  const arbitrary = fc.integer().chain((maximum: number) => {
    return fc.tuple(
      fc.nat(10),
      fc.constant(maximum),
      fc.integer({
        max: maximum
      }),
      fc.integer({
        max: maximum
      })
    )
  })

  fc.assert(fc.property(arbitrary, ([ offset, maximum, value, multiplier ]): boolean => {
    fc.pre(value % multiplier === 0)
    const context: EncodingContext = getDefaultEncodingContext()
    const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(offset + 8))
    const bytesWritten: number =
      ENCODE_ROOF_MULTIPLE_MIRROR_ENUM_VARINT(buffer, offset, value, {
        maximum, multiplier
      }, context)
    const result: IntegerResult =
      DECODE_ROOF_MULTIPLE_MIRROR_ENUM_VARINT(buffer, offset, {
        maximum, multiplier
      })
    return bytesWritten > 0 && result.bytes === bytesWritten && result.value === value
  }), {
    verbose: false
  })

  test.end()
})

tap.test('ARBITRARY_MULTIPLE_ZIGZAG_VARINT with multiplier 1', (test) => {
  fc.assert(fc.property(fc.nat(10), fc.integer(), (offset: number, value: number): boolean => {
    const context: EncodingContext = getDefaultEncodingContext()
    const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(offset + 8))
    const bytesWritten: number =
      ENCODE_ARBITRARY_MULTIPLE_ZIGZAG_VARINT(buffer, offset, value, { multiplier: 1 }, context)
    const result: IntegerResult =
      DECODE_ARBITRARY_MULTIPLE_ZIGZAG_VARINT(buffer, offset, { multiplier: 1 })
    return bytesWritten > 0 && result.bytes === bytesWritten && result.value === value
  }), {
    verbose: false
  })

  test.end()
})

tap.test('ARBITRARY_MULTIPLE_ZIGZAG_VARINT', (test) => {
  fc.assert(fc.property(fc.nat(10), fc.integer(), fc.integer(), (
    offset: number, value: number, multiplier: number
  ): boolean => {
    fc.pre(value % multiplier === 0)
    const context: EncodingContext = getDefaultEncodingContext()
    const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(offset + 8))
    const bytesWritten: number =
      ENCODE_ARBITRARY_MULTIPLE_ZIGZAG_VARINT(buffer, offset, value, {
        multiplier
      }, context)
    const result: IntegerResult =
      DECODE_ARBITRARY_MULTIPLE_ZIGZAG_VARINT(buffer, offset, {
        multiplier
      })
    return bytesWritten > 0 && result.bytes === bytesWritten && result.value === value
  }), {
    verbose: false
  })

  test.end()
})
