// Copyright 2025-present 650 Industries. All rights reserved.

import CoreGraphics
import ExpoModulesJSI
import Testing

@Suite("JavaScriptCodable+Primitives")
@JavaScriptActor
struct JavaScriptCodablePrimitivesTests {
  let runtime = JavaScriptRuntime()

  // `Number.MAX_SAFE_INTEGER` (2^53 - 1): the largest integer a JS number round-trips exactly.
  static let maxSafeInteger: Int64 = (1 << 53) - 1
  // 2^53 + 1: the first integer above the safe range, which a JS number can no longer represent.
  static let firstUnsafeInteger: Int64 = (1 << 53) + 1

  // MARK: - Double

  @Test
  func `decodes a double from a JS number`() throws {
    let value = try runtime.eval("3.5")
    let decoded = try Double.decode(value, in: runtime)
    #expect(decoded == 3.5)
  }

  @Test
  func `encodes a double to a JS number`() throws {
    let encoded = try Double.encode(2.5, in: runtime)
    #expect(encoded.getDouble() == 2.5)
  }

  @Test
  func `round-trips a double`() throws {
    let value = try runtime.eval("42.25")
    let decoded = try Double.decode(value, in: runtime)
    let reencoded = try Double.encode(decoded, in: runtime)
    #expect(reencoded.getDouble() == 42.25)
  }

  // MARK: - Int

  @Test
  func `decodes and encodes an int`() throws {
    let decoded = try Int.decode(runtime.eval("42"), in: runtime)
    #expect(decoded == 42)
    let encoded = try Int.encode(-7, in: runtime)
    #expect(encoded.getInt() == -7)
  }

  // MARK: - Integer family (representative widths)

  @Test
  func `decodes and encodes narrow and unsigned integers`() throws {
    #expect(try Int8.decode(runtime.eval("127"), in: runtime) == 127)
    #expect(try UInt8.decode(runtime.eval("255"), in: runtime) == 255)
    #expect(try Int64.decode(runtime.eval("1024"), in: runtime) == 1024)
    #expect(try UInt32.encode(300, in: runtime).getInt() == 300)
  }

  // MARK: - 64-bit integers and BigInt

  @Test
  func `encodes Int64 and UInt64 as a JS bigint regardless of magnitude`() throws {
    let smallSigned = try Int64.encode(5, in: runtime)
    #expect(smallSigned.isBigInt())
    #expect(try smallSigned.asBigInt().asInt64() == 5)

    let smallUnsigned = try UInt64.encode(5, in: runtime)
    #expect(smallUnsigned.isBigInt())
    #expect(try smallUnsigned.asBigInt().asUint64() == 5)
  }

  @Test
  func `round-trips Int64 and UInt64 values beyond the JS safe-integer range losslessly`() throws {
    // A magnitude above the safe-integer range would silently lose precision if it went through `.number`.
    let signed = Self.firstUnsafeInteger
    let signedEncoded = try Int64.encode(signed, in: runtime)
    #expect(try Int64.decode(signedEncoded, in: runtime) == signed)

    let unsigned = UInt64.max
    let unsignedEncoded = try UInt64.encode(unsigned, in: runtime)
    #expect(try UInt64.decode(unsignedEncoded, in: runtime) == unsigned)
  }

  @Test
  func `decodes Int64 and UInt64 from a JS bigint`() throws {
    #expect(try Int64.decode(runtime.eval("-\(Self.firstUnsafeInteger)n"), in: runtime) == -Self.firstUnsafeInteger)
    #expect(try UInt64.decode(runtime.eval("\(UInt64.max)n"), in: runtime) == UInt64.max)
  }

  @Test
  func `Int and UInt encode as a JS number within the safe-integer range`() throws {
    let small = try Int.encode(5, in: runtime)
    #expect(small.isNumber())
    #expect(small.getInt() == 5)

    let unsigned = try UInt.encode(UInt(Self.maxSafeInteger), in: runtime)
    #expect(unsigned.isNumber())
    #expect(unsigned.getDouble() == Double(Self.maxSafeInteger))
  }

  @Test
  func `Int and UInt encode throws beyond the JS safe-integer range`() throws {
    // Above the safe-integer range a JS number can't represent the value exactly; Int maps to a number,
    // so encoding throws rather than silently losing precision. Int64/UInt64 carry such values as a bigint.
    #expect(throws: UnsafeIntegerException.self) {
      _ = try Int.encode(Int(Self.firstUnsafeInteger), in: runtime)
    }
    #expect(throws: UnsafeIntegerException.self) {
      _ = try UInt.encode(UInt.max, in: runtime)
    }
  }

  @Test
  func `Int and UInt decode from a JS bigint`() throws {
    #expect(try Int.decode(runtime.eval("\(Self.firstUnsafeInteger)n"), in: runtime) == Int(Self.firstUnsafeInteger))
    #expect(try UInt.decode(runtime.eval("\(Self.firstUnsafeInteger)n"), in: runtime) == UInt(Self.firstUnsafeInteger))
  }

  @Test
  func `64-bit decode throws instead of trapping on an out-of-range bigint`() throws {
    // 2^64 overflows UInt64, and a negative bigint can't be a UInt64.
    #expect(throws: BigIntOutOfRangeException.self) {
      _ = try UInt64.decode(runtime.eval("18446744073709551616n"), in: runtime)
    }
    #expect(throws: BigIntOutOfRangeException.self) {
      _ = try UInt64.decode(runtime.eval("-1n"), in: runtime)
    }
  }

  @Test
  func `rounds a fractional number when decoding an integer, matching v1`() throws {
    // v1 `DynamicNumberType` rounds before narrowing, so 1.6 -> 2 (not truncated to 1).
    #expect(try Int.decode(runtime.eval("1.6"), in: runtime) == 2)
    #expect(try Int.decode(runtime.eval("1.4"), in: runtime) == 1)
    #expect(try Int.decode(runtime.eval("-1.6"), in: runtime) == -2)
  }

  @Test
  func `integer decode throws instead of trapping on out-of-range or non-finite numbers`() throws {
    #expect(throws: IntegerOutOfRangeException.self) {
      _ = try Int8.decode(runtime.eval("200"), in: runtime)
    }
    #expect(throws: IntegerOutOfRangeException.self) {
      _ = try Int.decode(runtime.eval("1e308"), in: runtime)
    }
    #expect(throws: IntegerOutOfRangeException.self) {
      _ = try Int.decode(runtime.eval("NaN"), in: runtime)
    }
  }

  // MARK: - Float / CGFloat

  @Test
  func `decodes and encodes float and cgfloat`() throws {
    #expect(try Float.decode(runtime.eval("1.5"), in: runtime) == 1.5)
    #expect(try CGFloat.encode(2.5, in: runtime).getDouble() == 2.5)
  }

  // MARK: - Bool

  @Test
  func `decodes and encodes a bool`() throws {
    #expect(try Bool.decode(runtime.eval("true"), in: runtime) == true)
    #expect(try Bool.encode(false, in: runtime).getBool() == false)
  }

  // MARK: - String

  @Test
  func `decodes and encodes a string`() throws {
    #expect(try String.decode(runtime.eval("'expo'"), in: runtime) == "expo")
    #expect(try String.encode("modules", in: runtime).getString() == "modules")
  }

  // MARK: - Zero-copy borrowed-value overload

  @Test
  func `decodes primitives from a borrowed argument buffer`() throws {
    // Each `decode` resolves to the borrowing `JavaScriptUnownedValue` overload (the fast path the
    // macro emits for arguments), exercising the per-type overrides rather than the owning form.
    let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: 42, 3.5, "expo", true)
    #expect(try Int.decode(buffer.unownedValue(at: 0), in: runtime) == 42)
    #expect(try Double.decode(buffer.unownedValue(at: 1), in: runtime) == 3.5)
    #expect(try String.decode(buffer.unownedValue(at: 2), in: runtime) == "expo")
    #expect(try Bool.decode(buffer.unownedValue(at: 3), in: runtime) == true)
  }

  @Test
  func `decodes a 64-bit bigint from a borrowed argument buffer`() throws {
    // The borrowing overload reads a `number` zero-copy but has to materialize an owning value on the
    // `bigint` branch; this exercises that materialization path for a value beyond the safe-integer range.
    let buffer = JavaScriptValuesBuffer.allocate(
      in: runtime,
      with: JavaScriptValue(runtime, bigInt: Self.firstUnsafeInteger),
      JavaScriptValue(runtime, bigInt: UInt64.max)
    )
    #expect(try Int64.decode(buffer.unownedValue(at: 0), in: runtime) == Self.firstUnsafeInteger)
    #expect(try UInt64.decode(buffer.unownedValue(at: 1), in: runtime) == UInt64.max)
  }

  // MARK: - Error paths

  @Test
  func `decode throws on a wrong-typed primitive instead of crashing`() throws {
    #expect(throws: JavaScriptValue.TypeError.self) {
      _ = try String.decode(runtime.eval("42"), in: runtime)
    }
    #expect(throws: JavaScriptValue.TypeError.self) {
      _ = try Double.decode(runtime.eval("'not a number'"), in: runtime)
    }
    #expect(throws: JavaScriptValue.TypeError.self) {
      _ = try Bool.decode(runtime.eval("'true'"), in: runtime)
    }
    // A wide integer decodes from either a number or a bigint; a value that is neither still throws a
    // TypeError rather than falling through.
    #expect(throws: JavaScriptValue.TypeError.self) {
      _ = try Int.decode(runtime.eval("'not a number'"), in: runtime)
    }
  }
}
