/*
 * 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 {
  ARBITRARY_TYPED_KEYS_OBJECT,
  NON_REQUIRED_BOUNDED_TYPED_OBJECT,
  REQUIRED_ONLY_BOUNDED_TYPED_OBJECT,
  MIXED_BOUNDED_TYPED_OBJECT,
  REQUIRED_UNBOUNDED_TYPED_OBJECT,
  OPTIONAL_UNBOUNDED_TYPED_OBJECT,
  MIXED_UNBOUNDED_TYPED_OBJECT,
  PACKED_UNBOUNDED_OBJECT,
  PACKED_BOUNDED_REQUIRED_OBJECT
} from '../../../lib/encoder/object/encode'

import {
  getEncoding
} from '../../../lib/mapper'

import {
  getStringEncoding
} from '../../../lib/mapper/string'

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

tap.test('ARBITRARY_TYPED_KEYS_OBJECT: should encode untyped {foo:"bar",baz:1}', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(16))
  const bytesWritten: number = ARBITRARY_TYPED_KEYS_OBJECT(buffer, 0, {
    foo: 'bar',
    baz: 1
  }, {
    keyEncoding: getStringEncoding({
      type: 'string'
    }, 1),
    encoding: {
      type: EncodingType.Any,
      encoding: 'ANY_PACKED_TYPE_TAG_BYTE_PREFIX',
      options: {}
    }
  }, context)

  test.strictSame(buffer.getBuffer(), Buffer.from([
    // Length
    0x02,

    // Key length + 'foo'
    0x04, 0x66, 0x6f, 0x6f,

    // String tag + length + 'bar'
    0x21, 0x62, 0x61, 0x72,

    // Key length + 'baz'
    0x04, 0x62, 0x61, 0x7a,

    // Value 1 + 1 with positive integer type tag
    0x15
  ]))

  test.is(bytesWritten, 14)
  test.end()
})

tap.test('ARBITRARY_TYPED_KEYS_OBJECT: should encode typed {foo:"bar",baz:1}', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(16))
  const bytesWritten: number = ARBITRARY_TYPED_KEYS_OBJECT(buffer, 0, {
    foo: 'bar',
    baz: 1
  }, {
    keyEncoding: getStringEncoding({
      type: 'string',
      minLength: 3
    }, 1),
    encoding: {
      type: EncodingType.Any,
      encoding: 'ANY_PACKED_TYPE_TAG_BYTE_PREFIX',
      options: {}
    }
  }, context)

  test.strictSame(buffer.getBuffer(), Buffer.from([
    // Length
    0x02,

    // Key length + 'foo'
    0x01, 0x66, 0x6f, 0x6f,

    // String tag + length + 'bar'
    0x21, 0x62, 0x61, 0x72,

    // Key length + 'baz'
    0x01, 0x62, 0x61, 0x7a,

    // Value 1 + 1 with positive integer type tag
    0x15
  ]))

  test.is(bytesWritten, 14)
  test.end()
})

tap.test('NON_REQUIRED_BOUNDED_TYPED_OBJECT: should encode typed {foo:"bar",baz:1}', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(7))
  const bytesWritten: number = NON_REQUIRED_BOUNDED_TYPED_OBJECT(buffer, 0, {
    foo: 'bar',
    baz: 1
  }, {
    optionalProperties: [ 'baz', 'bar', 'foo', 'qux' ],
    propertyEncodings: {
      foo: getEncoding({
        type: 'string'
      }, 1),
      baz: getEncoding({
        type: 'integer',
        minimum: 0
      }, 1),
      bar: getEncoding({}, 1),
      qux: getEncoding({}, 1)
    }
  }, context)

  test.strictSame(buffer.getBuffer(), Buffer.from([
    // Length
    0x04,

    // Bit set
    0b00000101,

    // 1
    0x01,

    // "bar"
    0x04, 0x62, 0x61, 0x72
  ]))

  test.is(bytesWritten, 7)
  test.end()
})

tap.test('NON_REQUIRED_BOUNDED_TYPED_OBJECT: should encode typed {}', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(2))
  const bytesWritten: number = NON_REQUIRED_BOUNDED_TYPED_OBJECT(buffer, 0, {}, {
    optionalProperties: [ 'baz', 'bar', 'foo', 'qux' ],
    propertyEncodings: {
      foo: getEncoding({
        type: 'string'
      }, 1),
      baz: getEncoding({
        type: 'integer',
        minimum: 0
      }, 1),
      bar: getEncoding({}, 1),
      qux: getEncoding({}, 1)
    }
  }, context)

  test.strictSame(buffer.getBuffer(), Buffer.from([
    // Length
    0x04,

    // Bit set
    0b00000000
  ]))

  test.is(bytesWritten, 2)
  test.end()
})

tap.test('REQUIRED_ONLY_BOUNDED_TYPED_OBJECT: should encode a bounded empty object', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(1))
  const bytesWritten: number = REQUIRED_ONLY_BOUNDED_TYPED_OBJECT(buffer, 0, {}, {
    propertyEncodings: {},
    requiredProperties: [],
    booleanRequiredProperties: []
  }, context)

  test.strictSame(buffer.getBuffer(), Buffer.from([]))
  test.is(bytesWritten, 0)
  test.end()
})

tap.test('REQUIRED_ONLY_BOUNDED_TYPED_OBJECT: should encode typed {foo:"bar",baz:1}', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(5))
  const bytesWritten: number = REQUIRED_ONLY_BOUNDED_TYPED_OBJECT(buffer, 0, {
    foo: 'bar',
    baz: 1
  }, {
    requiredProperties: [ 'baz', 'foo' ],
    booleanRequiredProperties: [],
    propertyEncodings: {
      foo: getEncoding({
        type: 'string'
      }, 1),
      baz: getEncoding({
        type: 'integer',
        minimum: 0
      }, 1)
    }
  }, context)

  test.strictSame(buffer.getBuffer(), Buffer.from([
    // 1
    0x01,

    // "bar"
    0x04, 0x62, 0x61, 0x72
  ]))

  test.is(bytesWritten, 5)
  test.end()
})

tap.test('REQUIRED_ONLY_BOUNDED_TYPED_OBJECT: should encode typed {foo:"bar",baz:1,baz:true,qux:false}', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(6))
  const bytesWritten: number = REQUIRED_ONLY_BOUNDED_TYPED_OBJECT(buffer, 0, {
    foo: 'bar',
    bar: 1,
    baz: true,
    qux: false
  }, {
    requiredProperties: [ 'bar', 'foo' ],
    booleanRequiredProperties: [ 'baz', 'qux' ],
    propertyEncodings: {
      foo: getEncoding({
        type: 'string'
      }, 1),
      bar: getEncoding({
        type: 'integer',
        minimum: 0
      }, 1),
      baz: getEncoding({
        type: 'boolean'
      }, 1),
      qux: getEncoding({
        type: 'boolean'
      }, 1)
    }
  }, context)

  test.strictSame(buffer.getBuffer(), Buffer.from([
    // Bitset
    0b00000001,

    // 1
    0x01,

    // "bar"
    0x04, 0x62, 0x61, 0x72
  ]))

  test.is(bytesWritten, 6)
  test.end()
})

tap.test('REQUIRED_ONLY_BOUNDED_TYPED_OBJECT: should encode three boolean properties', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(1))
  const bytesWritten: number = REQUIRED_ONLY_BOUNDED_TYPED_OBJECT(buffer, 0, {
    foo: true,
    bar: false,
    baz: true
  }, {
    requiredProperties: [],
    booleanRequiredProperties: [ 'foo', 'bar', 'baz' ],
    propertyEncodings: {
      foo: getEncoding({
        type: 'boolean'
      }, 1),
      bar: getEncoding({
        type: 'boolean'
      }, 1),
      baz: getEncoding({
        type: 'boolean'
      }, 1)
    }
  }, context)

  test.strictSame(buffer.getBuffer(), Buffer.from([ 0b00000101 ]))
  test.is(bytesWritten, 1)
  test.end()
})

tap.test('REQUIRED_ONLY_BOUNDED_TYPED_OBJECT: should encode eight boolean properties', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(1))
  const bytesWritten: number = REQUIRED_ONLY_BOUNDED_TYPED_OBJECT(buffer, 0, {
    foo: true,
    bar: false,
    baz: true,
    qux: true,
    xxx: false,
    yyy: false,
    zzz: true,
    qqq: true
  }, {
    requiredProperties: [],
    booleanRequiredProperties: [ 'foo', 'bar', 'baz', 'qux', 'xxx', 'yyy', 'zzz', 'qqq' ],
    propertyEncodings: {
      foo: getEncoding({
        type: 'boolean'
      }, 1),
      bar: getEncoding({
        type: 'boolean'
      }, 1),
      baz: getEncoding({
        type: 'boolean'
      }, 1),
      qux: getEncoding({
        type: 'boolean'
      }, 1),
      xxx: getEncoding({
        type: 'boolean'
      }, 1),
      yyy: getEncoding({
        type: 'boolean'
      }, 1),
      zzz: getEncoding({
        type: 'boolean'
      }, 1),
      qqq: getEncoding({
        type: 'boolean'
      }, 1)
    }
  }, context)

  test.strictSame(buffer.getBuffer(), Buffer.from([ 0b11001101 ]))
  test.is(bytesWritten, 1)
  test.end()
})

tap.test('REQUIRED_ONLY_BOUNDED_TYPED_OBJECT: should encode nine boolean properties', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(2))
  const bytesWritten: number = REQUIRED_ONLY_BOUNDED_TYPED_OBJECT(buffer, 0, {
    foo: true,
    bar: false,
    baz: true,
    qux: true,
    xxx: false,
    yyy: false,
    zzz: true,
    qqq: true,
    ppp: false
  }, {
    requiredProperties: [],
    booleanRequiredProperties: [ 'foo', 'bar', 'baz', 'qux', 'xxx', 'yyy', 'zzz', 'qqq', 'ppp' ],
    propertyEncodings: {
      foo: getEncoding({
        type: 'boolean'
      }, 1),
      bar: getEncoding({
        type: 'boolean'
      }, 1),
      baz: getEncoding({
        type: 'boolean'
      }, 1),
      qux: getEncoding({
        type: 'boolean'
      }, 1),
      xxx: getEncoding({
        type: 'boolean'
      }, 1),
      yyy: getEncoding({
        type: 'boolean'
      }, 1),
      zzz: getEncoding({
        type: 'boolean'
      }, 1),
      qqq: getEncoding({
        type: 'boolean'
      }, 1),
      ppp: getEncoding({
        type: 'boolean'
      }, 1)
    }
  }, context)

  test.strictSame(buffer.getBuffer(), Buffer.from([ 0b11001101, 0x00 ]))
  test.is(bytesWritten, 2)
  test.end()
})

tap.test('MIXED_BOUNDED_TYPED_OBJECT: should encode typed {foo:"bar",baz:1} with one required', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(7))
  const bytesWritten: number = MIXED_BOUNDED_TYPED_OBJECT(buffer, 0, {
    foo: 'bar',
    baz: 1
  }, {
    requiredProperties: [ 'foo' ],
    booleanRequiredProperties: [],
    optionalProperties: [ 'baz' ],
    propertyEncodings: {
      foo: getEncoding({
        type: 'string'
      }, 1),
      baz: getEncoding({
        type: 'integer',
        minimum: 0
      }, 1)
    }
  }, context)

  test.strictSame(buffer.getBuffer(), Buffer.from([
    // "bar"
    0x04, 0x62, 0x61, 0x72,

    // Bit map
    0x01, 0x01,

    // 1
    0x01
  ]))

  test.is(bytesWritten, 7)
  test.end()
})

tap.test('MIXED_BOUNDED_TYPED_OBJECT: should encode typed {foo:"bar",baz:1} with one missing optional', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(6))
  const bytesWritten: number = MIXED_BOUNDED_TYPED_OBJECT(buffer, 0, {
    foo: 'bar'
  }, {
    requiredProperties: [ 'foo' ],
    booleanRequiredProperties: [],
    optionalProperties: [ 'baz' ],
    propertyEncodings: {
      foo: getEncoding({
        type: 'string'
      }, 1),
      baz: getEncoding({
        type: 'integer',
        minimum: 0
      }, 1)
    }
  }, context)

  test.strictSame(buffer.getBuffer(), Buffer.from([
    // "bar"
    0x04, 0x62, 0x61, 0x72,

    // Bit map
    0x01, 0x00
  ]))

  test.is(bytesWritten, 6)
  test.end()
})

tap.test('REQUIRED_UNBOUNDED_TYPED_OBJECT: should encode semityped {foo:"bar",baz:1}', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(11))
  const bytesWritten: number = REQUIRED_UNBOUNDED_TYPED_OBJECT(buffer, 0, {
    foo: 'bar',
    baz: 1
  }, {
    requiredProperties: [ 'foo' ],
    booleanRequiredProperties: [],
    propertyEncodings: {
      foo: getEncoding({
        type: 'string'
      }, 1)
    },
    keyEncoding: getStringEncoding({
      type: 'string'
    }, 1),
    encoding: {
      type: EncodingType.Any,
      encoding: 'ANY_PACKED_TYPE_TAG_BYTE_PREFIX',
      options: {}
    }
  }, context)

  test.strictSame(buffer.getBuffer(), Buffer.from([
    // "bar"
    0x04, 0x62, 0x61, 0x72,

    // Length
    0x01,

    // Key length + 'baz'
    0x04, 0x62, 0x61, 0x7a,

    // Value 1 + 1 with positive integer type tag
    0x15
  ]))

  test.is(bytesWritten, 10)
  test.end()
})

tap.test('REQUIRED_UNBOUNDED_TYPED_OBJECT: should encode typed {foo:"bar"}', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(5))
  const bytesWritten: number = REQUIRED_UNBOUNDED_TYPED_OBJECT(buffer, 0, {
    foo: 'bar'
  }, {
    requiredProperties: [ 'foo' ],
    booleanRequiredProperties: [],
    propertyEncodings: {
      foo: getEncoding({
        type: 'string'
      }, 1)
    },
    keyEncoding: getStringEncoding({
      type: 'string'
    }, 1),
    encoding: {
      type: EncodingType.Any,
      encoding: 'ANY_PACKED_TYPE_TAG_BYTE_PREFIX',
      options: {}
    }
  }, context)

  test.strictSame(buffer.getBuffer(), Buffer.from([
    // "bar"
    0x04, 0x62, 0x61, 0x72,

    // Length
    0x00
  ]))

  test.is(bytesWritten, 5)
  test.end()
})

tap.test('OPTIONAL_UNBOUNDED_TYPED_OBJECT: should encode semityped {foo:"bar",baz:1}', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(13))
  const bytesWritten: number = OPTIONAL_UNBOUNDED_TYPED_OBJECT(buffer, 0, {
    foo: 'bar',
    baz: 1
  }, {
    optionalProperties: [ 'foo' ],
    propertyEncodings: {
      foo: getEncoding({
        type: 'string'
      }, 1)
    },
    keyEncoding: getStringEncoding({
      type: 'string'
    }, 1),
    encoding: {
      type: EncodingType.Any,
      encoding: 'ANY_PACKED_TYPE_TAG_BYTE_PREFIX',
      options: {}
    }
  }, context)

  test.strictSame(buffer.getBuffer(), Buffer.from([
    // Bit map
    0x01, 0x01,

    // "bar"
    0x04, 0x62, 0x61, 0x72,

    // Length
    0x01,

    // Key length + 'baz'
    0x04, 0x62, 0x61, 0x7a,

    // Value 1 + 1 with positive integer type tag
    0x15
  ]))

  test.is(bytesWritten, 12)
  test.end()
})

tap.test('MIXED_UNBOUNDED_TYPED_OBJECT: should encode mixed {foo:"bar",baz:1,qux:null}', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(13))
  const bytesWritten: number = MIXED_UNBOUNDED_TYPED_OBJECT(buffer, 0, {
    foo: 'bar',
    baz: 1,
    qux: null
  }, {
    requiredProperties: [ 'foo' ],
    booleanRequiredProperties: [],
    optionalProperties: [ 'baz' ],
    keyEncoding: getStringEncoding({
      type: 'string'
    }, 1),
    encoding: {
      type: EncodingType.Any,
      encoding: 'ANY_PACKED_TYPE_TAG_BYTE_PREFIX',
      options: {}
    },
    propertyEncodings: {
      foo: getEncoding({
        type: 'string'
      }, 1),
      baz: getEncoding({
        type: 'integer',
        minimum: 0
      }, 1)
    }
  }, context)

  test.strictSame(buffer.getBuffer(), Buffer.from([
    // "bar"
    0x04, 0x62, 0x61, 0x72,

    // Bit map
    0x01, 0x01,

    // 1
    0x01,

    // Free form count
    0x01,

    // "qux"
    0x04, 0x71, 0x75, 0x78,

    // Null type tag
    0x17
  ]))

  test.is(bytesWritten, 13)
  test.end()
})

tap.test('PACKED_UNBOUNDED_OBJECT: should encode a complex object', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(21))
  const bytesWritten: number = PACKED_UNBOUNDED_OBJECT(buffer, 0, {
    foo: 1,
    bar: 2,
    baz: 0,
    qux: 2,
    extra: 1,
    name: 'john',
    flag: true,
    random: 'x'
  }, {
    packedRequiredProperties: [ 'bar', 'baz', 'extra', 'foo', 'qux' ],
    packedEncoding: {
      type: EncodingType.Integer,
      encoding: 'BOUNDED_MULTIPLE_8BITS_ENUM_FIXED',
      options: {
        minimum: 0,
        maximum: 2,
        multiplier: 1
      }
    },
    encoding: {
      type: EncodingType.Any,
      encoding: 'ANY_PACKED_TYPE_TAG_BYTE_PREFIX',
      options: {}
    },
    propertyEncodings: {
      name: getEncoding({
        type: 'string'
      }, 1),
      age: getEncoding({
        type: 'integer',
        minimum: 0
      }, 1),
      flag: getEncoding({
        type: 'boolean'
      }, 1)
    },
    optionalProperties: [ 'age' ],
    requiredProperties: [ 'name' ],
    booleanRequiredProperties: [ 'flag' ],
    keyEncoding: getStringEncoding({
      type: 'string'
    }, 1)
  }, context)

  test.strictSame(buffer.getBuffer(), Buffer.from([
    // Amount of packed integers
    0x05,

    // REVERSE(10 (bar) 00 (baz) 01 (extra) 01 (foo))
    0b10100001,

    // REVERSE(10 (qux) 000000)
    0b00000001,

    // True (flag)
    0x01,

    // String length (4)
    0x05,

    // 'john'
    0x6a, 0x6f, 0x68, 0x6e,

    // Optional bitset length = 1
    0x01,

    // Optional properties bitset = 0
    0x00,

    // Extra properties length = 1
    0x01,

    // String length (6)
    0x07,

    // 'random'
    0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d,

    // 'x' (random)
    0x11, 0x78
  ]))

  test.is(bytesWritten, 21)
  test.end()
})

tap.test('PACKED_BOUNDED_REQUIRED_OBJECT: should encode a complex object', (test) => {
  const context: EncodingContext = getDefaultEncodingContext()
  const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(8))
  const bytesWritten: number = PACKED_BOUNDED_REQUIRED_OBJECT(buffer, 0, {
    foo: 1,
    bar: 2,
    baz: 0,
    qux: 2,
    extra: 1,
    name: 'john',
    flag: true
  }, {
    packedRequiredProperties: [ 'bar', 'baz', 'extra', 'foo', 'qux' ],
    packedEncoding: {
      type: EncodingType.Integer,
      encoding: 'BOUNDED_MULTIPLE_8BITS_ENUM_FIXED',
      options: {
        minimum: 0,
        maximum: 2,
        multiplier: 1
      }
    },
    propertyEncodings: {
      name: getEncoding({
        type: 'string'
      }, 1),
      flag: getEncoding({
        type: 'boolean'
      }, 1)
    },
    requiredProperties: [ 'name' ],
    booleanRequiredProperties: [ 'flag' ]
  }, context)

  test.strictSame(buffer.getBuffer(), Buffer.from([
    // REVERSE(10 (bar) 00 (baz) 01 (extra) 01 (foo))
    0b10100001,

    // REVERSE(10 (qux) 000000)
    0b00000001,

    // True (flag)
    0x01,

    // String length (4)
    0x05,

    // 'john'
    0x6a, 0x6f, 0x68, 0x6e
  ]))

  test.is(bytesWritten, 8)
  test.end()
})
