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

import assert from 'assert';
import ioc, { createPreciseReader, DataType } from '../../../lib/structure/io/binary/GraphBinary.js';
import StreamReader from '../../../lib/structure/io/binary/internals/StreamReader.js';
import { Float, Double, Int, Long, Short, Byte, toFloat, toDouble, toInt, toLong, toShort, toByte, unwrap } from '../../../lib/utils.js';
import Connection from '../../../lib/driver/connection.js';
import { Path } from '../../../lib/structure/graph.js';

const { anySerializer, graphBinaryReader } = ioc;

describe('Precise Mode Tests', () => {
  let preciseReader;

  before(() => {
    preciseReader = createPreciseReader();
  });

  async function deserializeWithPrecise(buf) {
    return preciseReader.ioc.anySerializer.deserialize(StreamReader.fromBuffer(buf));
  }

  async function deserializeWithDefault(buf) {
    return anySerializer.deserialize(StreamReader.fromBuffer(buf));
  }

  describe('Basic deserialization', () => {
    it('FLOAT bytes → Float instance', async () => {
      const buf = anySerializer.serialize(toFloat(1.5));
      const result = await deserializeWithPrecise(buf);
      assert.ok(result instanceof Float);
      assert.strictEqual(result.value, 1.5);
      assert.strictEqual(result.type, 'float');
    });

    it('DOUBLE bytes → Double instance', async () => {
      const buf = anySerializer.serialize(toDouble(3.14));
      const result = await deserializeWithPrecise(buf);
      assert.ok(result instanceof Double);
      assert.strictEqual(result.value, 3.14);
      assert.strictEqual(result.type, 'double');
    });

    it('INT bytes → Int instance', async () => {
      const buf = anySerializer.serialize(toInt(42));
      const result = await deserializeWithPrecise(buf);
      assert.ok(result instanceof Int);
      assert.strictEqual(result.value, 42);
      assert.strictEqual(result.type, 'int');
    });

    it('LONG bytes (safe range) → Long instance with number value', async () => {
      const buf = anySerializer.serialize(toLong(42));
      const result = await deserializeWithPrecise(buf);
      assert.ok(result instanceof Long);
      assert.strictEqual(result.value, 42);
      assert.strictEqual(result.type, 'long');
    });

    it('LONG bytes (unsafe range) → Long instance with bigint value', async () => {
      const buf = anySerializer.serialize(toLong(9007199254740993n));
      const result = await deserializeWithPrecise(buf);
      assert.ok(result instanceof Long);
      assert.strictEqual(result.value, 9007199254740993n);
    });

    it('SHORT bytes → Short instance', async () => {
      const buf = anySerializer.serialize(toShort(5));
      const result = await deserializeWithPrecise(buf);
      assert.ok(result instanceof Short);
      assert.strictEqual(result.value, 5);
      assert.strictEqual(result.type, 'short');
    });

    it('BYTE bytes → Byte instance', async () => {
      const buf = anySerializer.serialize(toByte(127));
      const result = await deserializeWithPrecise(buf);
      assert.ok(result instanceof Byte);
      assert.strictEqual(result.value, 127);
      assert.strictEqual(result.type, 'byte');
    });
  });

  describe('Nested structures', () => {
    it('MAP containing Float values → Float wrappers inside Map', async () => {
      const map = new Map([['x', toFloat(1.5)]]);
      const buf = anySerializer.serialize(map);
      const result = await deserializeWithPrecise(buf);
      assert.ok(result.get('x') instanceof Float);
      assert.strictEqual(result.get('x').value, 1.5);
    });

    it('LIST with mixed numeric types → each wrapped correctly', async () => {
      const list = [toFloat(1.5), toInt(2), toDouble(3.14)];
      const buf = anySerializer.serialize(list);
      const result = await deserializeWithPrecise(buf);
      assert.ok(result[0] instanceof Float);
      assert.ok(result[1] instanceof Int);
      assert.ok(result[2] instanceof Double);
    });

    it('PATH with numeric vertex IDs → wrapped correctly', async () => {
      const path = new Path([['a'], ['b']], [toInt(1), toInt(2)]);
      const buf = anySerializer.serialize(path);
      const result = await deserializeWithPrecise(buf);
      assert.ok(result instanceof Path);
      assert.ok(result.objects[0] instanceof Int);
      assert.strictEqual(result.objects[0].value, 1);
      assert.ok(result.objects[1] instanceof Int);
      assert.strictEqual(result.objects[1].value, 2);
    });

    it('LIST with Short, Byte, Long → each wrapped correctly', async () => {
      const list = [toShort(5), toByte(127), toLong(99)];
      const buf = anySerializer.serialize(list);
      const result = await deserializeWithPrecise(buf);
      assert.ok(result[0] instanceof Short);
      assert.strictEqual(result[0].value, 5);
      assert.ok(result[1] instanceof Byte);
      assert.strictEqual(result[1].value, 127);
      assert.ok(result[2] instanceof Long);
      assert.strictEqual(result[2].value, 99);
    });

    it('Non-numeric types still deserialize correctly', async () => {
      const list = ['hello', true, 'world'];
      const buf = anySerializer.serialize(list);
      const result = await deserializeWithPrecise(buf);
      assert.deepStrictEqual(result, ['hello', true, 'world']);
    });
  });

  describe('Precise reader error paths', () => {
    it('returns null for a null-flagged numeric value', async () => {
      const buf = Buffer.from([DataType.INT, 0x01]);
      const result = await deserializeWithPrecise(buf);
      assert.strictEqual(result, null);
    });

    it('returns null for a null-flagged LONG value', async () => {
      const buf = Buffer.from([DataType.LONG, 0x01]);
      assert.strictEqual(await deserializeWithPrecise(buf), null);
    });

    it('returns null for a null-flagged FLOAT value', async () => {
      const buf = Buffer.from([DataType.FLOAT, 0x01]);
      assert.strictEqual(await deserializeWithPrecise(buf), null);
    });

    it('returns null for a null-flagged DOUBLE value', async () => {
      const buf = Buffer.from([DataType.DOUBLE, 0x01]);
      assert.strictEqual(await deserializeWithPrecise(buf), null);
    });

    it('returns null for a null-flagged SHORT value', async () => {
      const buf = Buffer.from([DataType.SHORT, 0x01]);
      assert.strictEqual(await deserializeWithPrecise(buf), null);
    });

    it('returns null for a null-flagged BYTE value', async () => {
      const buf = Buffer.from([DataType.BYTE, 0x01]);
      assert.strictEqual(await deserializeWithPrecise(buf), null);
    });

    it('throws on invalid value_flag', async () => {
      const buf = Buffer.from([DataType.INT, 0xFF, 0, 0, 0, 0]);
      await assert.rejects(() => deserializeWithPrecise(buf), /AnySerializer: unexpected \{value_flag}=0x/);
    });

    it('throws on unknown type_code', async () => {
      const buf = Buffer.from([0xEE, 0x00]);
      await assert.rejects(() => deserializeWithPrecise(buf), /AnySerializer: unknown \{type_code}=0xee/);
    });

    it('deserializes value_flag 0x02 as non-null', async () => {
      const buf = Buffer.from([DataType.INT, 0x02, 0x00, 0x00, 0x00, 0x2A]);
      const result = await deserializeWithPrecise(buf);
      assert.ok(result instanceof Int);
      assert.strictEqual(result.value, 42);
    });

    it('wraps deserializeValue errors with position info', async () => {
      const buf = Buffer.from([DataType.INT, 0x00]);
      await assert.rejects(() => deserializeWithPrecise(buf), /IntSerializer\.deserializeValue\(\) at position \d+/);
    });
  });

  describe('Wrapper behavior', () => {
    it('Float valueOf works in arithmetic', () => {
      assert.strictEqual(new Float(1.5) + 1, 2.5);
    });

    it('Int valueOf works in arithmetic', () => {
      assert.strictEqual(new Int(42) + 0, 42);
    });

    it('Double valueOf works in arithmetic', () => {
      assert.strictEqual(new Double(3.14) + 0, 3.14);
    });

    it('Short valueOf works in arithmetic', () => {
      assert.strictEqual(new Short(5) + 1, 6);
    });

    it('Byte valueOf works in arithmetic', () => {
      assert.strictEqual(new Byte(127) + 0, 127);
    });

    it('Long with unsafe bigint throws RangeError on arithmetic', () => {
      assert.throws(() => new Long(9007199254740993n) + 1, RangeError);
    });

    it('Long with unsafe string throws RangeError on arithmetic', () => {
      assert.throws(() => new Long('9007199254740993') + 1, RangeError);
    });

    it('Long with safe bigint works in arithmetic', () => {
      assert.strictEqual(new Long(42n) + 1, 43);
    });

    it('Long toPrimitive string hint works for unsafe values', () => {
      assert.strictEqual(`${new Long(9007199254740993n)}`, '9007199254740993');
    });

    it('Float toPrimitive string hint', () => {
      assert.strictEqual(`${new Float(1.5)}`, '1.5');
    });

    it('Double toPrimitive string hint', () => {
      assert.strictEqual(`${new Double(3.14)}`, '3.14');
    });

    it('Int toPrimitive string hint', () => {
      assert.strictEqual(`${new Int(67)}`, '67');
    });

    it('Short toPrimitive string hint', () => {
      assert.strictEqual(`${new Short(5)}`, '5');
    });

    it('Byte toPrimitive string hint', () => {
      assert.strictEqual(`${new Byte(127)}`, '127');
    });

    it('JSON.stringify Float', () => {
      assert.strictEqual(JSON.stringify(new Float(1.5)), '1.5');
    });

    it('JSON.stringify Long with bigint', () => {
      assert.strictEqual(JSON.stringify(new Long(9007199254740993n)), '"9007199254740993"');
    });

    it('Long valueOf with safe string returns number', () => {
      assert.strictEqual(new Long('42').valueOf(), 42);
    });

    it('Long valueOf with unsafe string throws RangeError', () => {
      assert.throws(() => new Long('9007199254740993').valueOf(), RangeError);
    });

    it('JSON.stringify Long with string value preserves string', () => {
      assert.strictEqual(JSON.stringify(new Long('9007199254740993')), '"9007199254740993"');
    });

    it('JSON.stringify Long with number value is a number', () => {
      assert.strictEqual(JSON.stringify(new Long(42)), '42');
    });

    it('JSON.stringify Double', () => {
      assert.strictEqual(JSON.stringify(new Double(3.14)), '3.14');
    });

    it('JSON.stringify Int', () => {
      assert.strictEqual(JSON.stringify(new Int(67)), '67');
    });

    it('JSON.stringify Short', () => {
      assert.strictEqual(JSON.stringify(new Short(5)), '5');
    });

    it('JSON.stringify Byte', () => {
      assert.strictEqual(JSON.stringify(new Byte(127)), '127');
    });

    it('unwrap Float', () => {
      assert.strictEqual(unwrap(new Float(1.5)), 1.5);
    });

    it('unwrap Long with bigint', () => {
      assert.strictEqual(unwrap(new Long(42n)), 42n);
    });

    it('unwrap Long with string', () => {
      assert.strictEqual(unwrap(new Long('123')), '123');
    });

    it('unwrap Long with number', () => {
      assert.strictEqual(unwrap(new Long(42)), 42);
    });

    it('unwrap Int', () => {
      assert.strictEqual(unwrap(new Int(42)), 42);
    });

    it('unwrap Double', () => {
      assert.strictEqual(unwrap(new Double(3.14)), 3.14);
    });

    it('unwrap Short', () => {
      assert.strictEqual(unwrap(new Short(5)), 5);
    });

    it('unwrap Byte', () => {
      assert.strictEqual(unwrap(new Byte(127)), 127);
    });

    it('unwrap plain number passthrough', () => {
      assert.strictEqual(unwrap(42), 42);
    });

    it('unwrap null passthrough', () => {
      assert.strictEqual(unwrap(null), null);
    });

    it('unwrap undefined passthrough', () => {
      assert.strictEqual(unwrap(undefined), undefined);
    });
  });

  describe('Long constructor validation', () => {
    it('rejects non-numeric string', () => {
      assert.throws(() => new Long('abc'), TypeError);
    });

    it('rejects injection attempt', () => {
      assert.throws(() => new Long('1L).drop()'), TypeError);
    });

    it('rejects empty string', () => {
      assert.throws(() => new Long(''), TypeError);
    });

    it('rejects non-integer number', () => {
      assert.throws(() => new Long(1.5), TypeError);
    });

    it('accepts exact int64 max (bigint)', () => {
      const l = new Long(9223372036854775807n);
      assert.strictEqual(l.value, 9223372036854775807n);
    });

    it('accepts exact int64 min (bigint)', () => {
      const l = new Long(-9223372036854775808n);
      assert.strictEqual(l.value, -9223372036854775808n);
    });

    it('rejects one above int64 max (bigint)', () => {
      assert.throws(() => new Long(9223372036854775808n), RangeError);
    });

    it('rejects one below int64 min (bigint)', () => {
      assert.throws(() => new Long(-9223372036854775809n), RangeError);
    });

    it('accepts exact int64 max (string)', () => {
      const l = new Long('9223372036854775807');
      assert.strictEqual(l.value, '9223372036854775807');
    });

    it('rejects one above int64 max (string)', () => {
      assert.throws(() => new Long('9223372036854775808'), RangeError);
    });

    it('rejects negative zero string', () => {
      assert.throws(() => new Long('-0'), TypeError);
    });

    it('accepts negative zero number', () => {
      const l = new Long(-0);
      assert.strictEqual(l.valueOf(), -0);
    });

    it('rejects leading zeros in string', () => {
      assert.throws(() => new Long('0042'), TypeError);
    });
  });

  describe('Float constructor validation', () => {
    it('rejects non-number argument', () => {
      assert.throws(() => new Float('1.5'), TypeError);
    });
  });

  describe('Double constructor validation', () => {
    it('rejects non-number argument', () => {
      assert.throws(() => new Double('3.14'), TypeError);
    });
  });

  describe('Int constructor validation', () => {
    it('accepts exact int32 max', () => {
      assert.strictEqual(new Int(2147483647).value, 2147483647);
    });

    it('accepts exact int32 min', () => {
      assert.strictEqual(new Int(-2147483648).value, -2147483648);
    });

    it('rejects above int32 max', () => {
      assert.throws(() => new Int(2147483648), RangeError);
    });

    it('rejects below int32 min', () => {
      assert.throws(() => new Int(-2147483649), RangeError);
    });

    it('rejects non-integer', () => {
      assert.throws(() => new Int(1.5), TypeError);
    });

    it('rejects non-number argument', () => {
      assert.throws(() => new Int('5'), TypeError);
    });
  });

  describe('Short constructor validation', () => {
    it('accepts exact int16 max', () => {
      assert.strictEqual(new Short(32767).value, 32767);
    });

    it('accepts exact int16 min', () => {
      assert.strictEqual(new Short(-32768).value, -32768);
    });

    it('rejects above int16 max', () => {
      assert.throws(() => new Short(32768), RangeError);
    });

    it('rejects below int16 min', () => {
      assert.throws(() => new Short(-32769), RangeError);
    });

    it('rejects non-integer', () => {
      assert.throws(() => new Short(1.5), TypeError);
    });

    it('rejects non-number argument', () => {
      assert.throws(() => new Short('5'), TypeError);
    });
  });

  describe('Byte constructor validation', () => {
    it('accepts exact int8 max', () => {
      assert.strictEqual(new Byte(127).value, 127);
    });

    it('accepts exact int8 min', () => {
      assert.strictEqual(new Byte(-128).value, -128);
    });

    it('rejects above int8 max', () => {
      assert.throws(() => new Byte(128), RangeError);
    });

    it('rejects below int8 min', () => {
      assert.throws(() => new Byte(-129), RangeError);
    });

    it('rejects non-integer', () => {
      assert.throws(() => new Byte(1.5), TypeError);
    });

    it('rejects non-number argument', () => {
      assert.throws(() => new Byte('127'), TypeError);
    });
  });

  describe('Backward compatibility', () => {
    it('default graphBinaryReader still returns plain numbers after createPreciseReader()', async () => {
      const buf = anySerializer.serialize(toFloat(1.5));
      const result = await deserializeWithDefault(buf);
      assert.strictEqual(result, 1.5);
      assert.ok(!(result instanceof Float));
    });
  });

  describe('Connection option wiring', () => {
    it('preciseNumbers: true uses a precise reader', () => {
      const conn = new Connection('http://localhost:8182', { preciseNumbers: true });
      assert.ok(conn._responseSerializer !== graphBinaryReader);
    });

    it('explicit responseSerializer takes precedence over preciseNumbers', () => {
      const customReader = { custom: true };
      const conn = new Connection('http://localhost:8182', { responseSerializer: customReader, preciseNumbers: true });
      assert.strictEqual(conn._responseSerializer, customReader);
    });

    it('default uses the default reader', () => {
      const conn = new Connection('http://localhost:8182', {});
      assert.ok(conn._responseSerializer instanceof graphBinaryReader.constructor);
    });
  });
});
