/*
 * 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 Fory, { Type } from '../packages/core/index';
import { describe, expect, test } from '@jest/globals';

describe('size-limit guardrails', () => {
  describe('configuration', () => {
    test('should have default limits matching Go', () => {
      const fory = new Fory();
      expect(fory.readContext.maxBinarySize).toBe(64 * 1024 * 1024);
      expect(fory.readContext.maxCollectionSize).toBe(1_000_000);
    });

    test('should accept custom maxBinarySize', () => {
      const fory = new Fory({ maxBinarySize: 1024 });
      expect(fory.readContext.maxBinarySize).toBe(1024);
    });

    test('should accept custom maxCollectionSize', () => {
      const fory = new Fory({ maxCollectionSize: 500 });
      expect(fory.readContext.maxCollectionSize).toBe(500);
    });

    test('should accept zero as a valid limit', () => {
      const fory = new Fory({ maxBinarySize: 0, maxCollectionSize: 0 });
      expect(fory.readContext.maxBinarySize).toBe(0);
      expect(fory.readContext.maxCollectionSize).toBe(0);
    });

    test('should reject negative maxBinarySize', () => {
      expect(() => new Fory({ maxBinarySize: -1 })).toThrow(
        'maxBinarySize must be a non-negative integer'
      );
    });

    test('should reject non-integer maxBinarySize', () => {
      expect(() => new Fory({ maxBinarySize: 1.5 })).toThrow(
        'maxBinarySize must be a non-negative integer'
      );
    });

    test('should reject NaN maxBinarySize', () => {
      expect(() => new Fory({ maxBinarySize: NaN })).toThrow(
        'maxBinarySize must be a non-negative integer'
      );
    });

    test('should reject negative maxCollectionSize', () => {
      expect(() => new Fory({ maxCollectionSize: -10 })).toThrow(
        'maxCollectionSize must be a non-negative integer'
      );
    });

    test('should reject non-integer maxCollectionSize', () => {
      expect(() => new Fory({ maxCollectionSize: 2.7 })).toThrow(
        'maxCollectionSize must be a non-negative integer'
      );
    });

    test('should work with other options combined', () => {
      const fory = new Fory({
        maxDepth: 100,
        maxBinarySize: 1024,
        maxCollectionSize: 500,
        ref: true,
        compatible: true,
      });
      expect(fory.readContext.maxDepth).toBe(100);
      expect(fory.readContext.maxBinarySize).toBe(1024);
      expect(fory.readContext.maxCollectionSize).toBe(500);
    });
  });

  describe('checkCollectionSize', () => {
    test('should not throw when size is within default limit', () => {
      const fory = new Fory();
      expect(() => fory.readContext.checkCollectionSize(999999)).not.toThrow();
    });

    test('should not throw when size is within limit', () => {
      const fory = new Fory({ maxCollectionSize: 100 });
      expect(() => fory.readContext.checkCollectionSize(100)).not.toThrow();
      expect(() => fory.readContext.checkCollectionSize(0)).not.toThrow();
    });

    test('should throw when size exceeds limit', () => {
      const fory = new Fory({ maxCollectionSize: 100 });
      expect(() => fory.readContext.checkCollectionSize(101)).toThrow(
        'Collection size 101 exceeds maxCollectionSize 100'
      );
    });

    test('error message should include helpful suggestion', () => {
      const fory = new Fory({ maxCollectionSize: 10 });
      expect(() => fory.readContext.checkCollectionSize(20)).toThrow(
        'increase maxCollectionSize if needed'
      );
    });
  });

  describe('checkBinarySize', () => {
    test('should not throw when size is within default limit', () => {
      const fory = new Fory();
      expect(() => fory.readContext.checkBinarySize(999999)).not.toThrow();
    });

    test('should not throw when size is within limit', () => {
      const fory = new Fory({ maxBinarySize: 1024 });
      expect(() => fory.readContext.checkBinarySize(1024)).not.toThrow();
      expect(() => fory.readContext.checkBinarySize(0)).not.toThrow();
    });

    test('should throw when size exceeds limit', () => {
      const fory = new Fory({ maxBinarySize: 1024 });
      expect(() => fory.readContext.checkBinarySize(1025)).toThrow(
        'Binary size 1025 exceeds maxBinarySize 1024'
      );
    });
  });

  describe('list deserialization with maxCollectionSize', () => {
    test('should deserialize list within limit', () => {
      const fory = new Fory({ maxCollectionSize: 10 });
      const { serialize, deserialize } = fory.register(Type.array(Type.int32()));
      const data = [1, 2, 3];
      const result = deserialize(serialize(data));
      expect(result).toEqual(data);
    });

    test('should throw when list exceeds maxCollectionSize', () => {
      const serializeFory = new Fory();
      const { serialize } = serializeFory.register(Type.array(Type.int32()));
      const bytes = serialize([1, 2, 3, 4, 5]);

      const deserializeFory = new Fory({ maxCollectionSize: 3 });
      const { deserialize } = deserializeFory.register(Type.array(Type.int32()));
      expect(() => deserialize(bytes)).toThrow('exceeds maxCollectionSize');
    });

    test('should deserialize list at exact limit', () => {
      const fory = new Fory({ maxCollectionSize: 3 });
      const { serialize, deserialize } = fory.register(Type.array(Type.int32()));
      const data = [1, 2, 3];
      const result = deserialize(serialize(data));
      expect(result).toEqual(data);
    });
  });

  describe('set deserialization with maxCollectionSize', () => {
    test('should deserialize set within limit', () => {
      const fory = new Fory({ maxCollectionSize: 10, ref: true });
      const { serialize, deserialize } = fory.register(Type.set(Type.int32()));
      const data = new Set([1, 2, 3]);
      const result = deserialize(serialize(data));
      expect(result).toEqual(data);
    });

    test('should throw when set exceeds maxCollectionSize', () => {
      const serializeFory = new Fory({ ref: true });
      const { serialize } = serializeFory.register(Type.set(Type.int32()));
      const bytes = serialize(new Set([1, 2, 3, 4, 5]));

      const deserializeFory = new Fory({ maxCollectionSize: 3, ref: true });
      const { deserialize } = deserializeFory.register(Type.set(Type.int32()));
      expect(() => deserialize(bytes)).toThrow('exceeds maxCollectionSize');
    });
  });

  describe('map deserialization with maxCollectionSize', () => {
    test('should deserialize map within limit', () => {
      const fory = new Fory({ maxCollectionSize: 10, ref: true });
      const { serialize, deserialize } = fory.register(
        Type.map(Type.string(), Type.int32())
      );
      const data = new Map([['a', 1], ['b', 2]]);
      const result = deserialize(serialize(data));
      expect(result).toEqual(data);
    });

    test('should throw when map exceeds maxCollectionSize', () => {
      const serializeFory = new Fory({ ref: true });
      const { serialize } = serializeFory.register(
        Type.map(Type.string(), Type.int32())
      );
      const bytes = serialize(new Map([['a', 1], ['b', 2], ['c', 3], ['d', 4]]));

      const deserializeFory = new Fory({ maxCollectionSize: 2, ref: true });
      const { deserialize } = deserializeFory.register(
        Type.map(Type.string(), Type.int32())
      );
      expect(() => deserialize(bytes)).toThrow('exceeds maxCollectionSize');
    });
  });

  describe('binary deserialization with maxBinarySize', () => {
    test('should deserialize binary within limit', () => {
      const fory = new Fory({ maxBinarySize: 1024, ref: true });
      const { serialize, deserialize } = fory.register(Type.struct("test.binary", {
        data: Type.binary(),
      }));
      const data = { data: new Uint8Array([1, 2, 3]) };
      const result = deserialize(serialize(data));
      expect(result!.data![0]).toBe(1);
      expect(result!.data![1]).toBe(2);
      expect(result!.data![2]).toBe(3);
    });

    test('should throw when binary exceeds maxBinarySize', () => {
      const serializeFory = new Fory({ ref: true });
      const { serialize } = serializeFory.register(Type.struct("test.binary2", {
        data: Type.binary(),
      }));
      const bytes = serialize({ data: new Uint8Array(100) });

      const deserializeFory = new Fory({ maxBinarySize: 50, ref: true });
      const { deserialize } = deserializeFory.register(Type.struct("test.binary2", {
        data: Type.binary(),
      }));
      expect(() => deserialize(bytes)).toThrow('exceeds maxBinarySize');
    });
  });

  describe('default limits allow normal payloads', () => {
    test('should allow large collections within default limit', () => {
      const fory = new Fory({ ref: true });
      const { serialize, deserialize } = fory.register(Type.array(Type.int32()));
      const bigArray = Array.from({ length: 1000 }, (_, i) => i);
      const result = deserialize(serialize(bigArray));
      expect(result).toEqual(bigArray);
    });
  });

  describe('polymorphic (any-typed) collection paths', () => {
    test('should enforce maxCollectionSize on untyped list', () => {
      const serializeFory = new Fory({ ref: true });
      const bytes = serializeFory.serialize([1, "two", 3.0]);

      const deserializeFory = new Fory({ maxCollectionSize: 2, ref: true });
      expect(() => deserializeFory.deserialize(bytes)).toThrow('exceeds maxCollectionSize');
    });

    test('should enforce maxCollectionSize on untyped map', () => {
      const serializeFory = new Fory({ ref: true });
      const bytes = serializeFory.serialize(new Map([["a", 1], ["b", 2], ["c", 3]]));

      const deserializeFory = new Fory({ maxCollectionSize: 2, ref: true });
      expect(() => deserializeFory.deserialize(bytes)).toThrow('exceeds maxCollectionSize');
    });
  });

  describe('bool array deserialization with maxCollectionSize', () => {
    // BoolArraySerializerGenerator reads an element count — guarded by checkCollectionSize
    test('should deserialize bool array within limit', () => {
      const fory = new Fory({ maxCollectionSize: 10 });
      const { serialize, deserialize } = fory.register(Type.struct("test.boolArr", {
        flags: Type.boolArray(),
      }));
      const data = { flags: [true, false, true] };
      const result = deserialize(serialize(data));
      expect(result!.flags).toEqual([true, false, true]);
    });

    test('should throw when bool array exceeds maxCollectionSize', () => {
      const serializeFory = new Fory();
      const { serialize } = serializeFory.register(Type.struct("test.boolArr2", {
        flags: Type.boolArray(),
      }));
      const bytes = serialize({ flags: [true, false, true, true, false] });

      const deserializeFory = new Fory({ maxCollectionSize: 3 });
      const { deserialize } = deserializeFory.register(Type.struct("test.boolArr2", {
        flags: Type.boolArray(),
      }));
      expect(() => deserialize(bytes)).toThrow('exceeds maxCollectionSize');
    });
  });

  describe('float16 array deserialization with maxBinarySize', () => {
    // Float16ArraySerializerGenerator writes byte count (elements * 2) — guarded by checkBinarySize
    test('should deserialize float16 array within limit', () => {
      const fory = new Fory({ maxBinarySize: 1024 });
      const { serialize, deserialize } = fory.register(Type.struct("test.f16Arr", {
        vals: Type.float16Array(),
      }));
      const data = { vals: [1.0, 2.0, 3.0] };
      const result = deserialize(serialize(data));
      expect(result!.vals!.length).toBe(3);
    });

    test('should throw when float16 array byte length exceeds maxBinarySize', () => {
      const serializeFory = new Fory();
      const { serialize } = serializeFory.register(Type.struct("test.f16Arr2", {
        vals: Type.float16Array(),
      }));
      // 10 elements × 2 bytes each = 20 raw bytes on the wire
      const bytes = serialize({ vals: Array.from({ length: 10 }, (_, i) => i * 0.5) });

      const deserializeFory = new Fory({ maxBinarySize: 10 }); // 10 < 20
      const { deserialize } = deserializeFory.register(Type.struct("test.f16Arr2", {
        vals: Type.float16Array(),
      }));
      expect(() => deserialize(bytes)).toThrow('exceeds maxBinarySize');
    });
  });

  describe('bfloat16 array deserialization with maxBinarySize', () => {
    // BFloat16ArraySerializerGenerator writes byte count (elements * 2) — same pattern as float16
    test('should deserialize bfloat16 array within limit', () => {
      const fory = new Fory({ maxBinarySize: 1024 });
      const { serialize, deserialize } = fory.register(Type.struct("test.bf16Arr", {
        vals: Type.bfloat16Array(),
      }));
      const data = { vals: [1.0, 2.0, 3.0] };
      const result = deserialize(serialize(data));
      expect(result!.vals!.length).toBe(3);
    });

    test('should throw when bfloat16 array byte length exceeds maxBinarySize', () => {
      const serializeFory = new Fory();
      const { serialize } = serializeFory.register(Type.struct("test.bf16Arr2", {
        vals: Type.bfloat16Array(),
      }));
      // 10 elements × 2 bytes each = 20 raw bytes on the wire
      const bytes = serialize({ vals: Array.from({ length: 10 }, (_, i) => i * 0.5) });

      const deserializeFory = new Fory({ maxBinarySize: 10 }); // 10 < 20
      const { deserialize } = deserializeFory.register(Type.struct("test.bf16Arr2", {
        vals: Type.bfloat16Array(),
      }));
      expect(() => deserialize(bytes)).toThrow('exceeds maxBinarySize');
    });
  });
});
