import { describe, expect, it } from "bun:test"
import { isColumn } from "./column"
import { InvalidValueError, MissingValueError, UntypedValueError } from "./errors"
import { TYPE_IDS } from "./types"
import {
  addr,
  bool,
  bytes,
  dec,
  decFromUnits,
  decUnits,
  I32_MAX,
  I32_MIN,
  i32,
  key,
  MAX_BYTES_LENGTH,
  str,
  toValue,
  U64_MAX,
  U256_MAX,
  u64,
  u256,
} from "./values"

const KEY = `0x${"ab".repeat(32)}` as const
const VITALIK = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"

describe("tagged constructors", () => {
  it("tag each value with the type it was built as", () => {
    const built = [
      [bool(true), "bool", true],
      [i32(10), "i32", 10],
      [u256(10n), "u256", 10n],
      [dec("3.5"), "dec", "3.5"],
      [str("Bob"), "str", "Bob"],
      [key(KEY), "key", KEY],
      [addr(VITALIK), "addr", VITALIK],
    ] as const
    for (const [value, tag, decoded] of built) {
      expect(value.type).toBe(tag)
      expect(value.value).toBe(decoded)
    }
  })

  it("keep the tag distinct from the wire typeId", () => {
    // The tag is what a client says; the typeId is what the wire carries.
    expect(TYPE_IDS[i32(1).type]).toBe(2)
    expect(TYPE_IDS[key(KEY).type]).toBe(10)
  })
})

describe("i32", () => {
  it("accepts the full signed 32-bit range", () => {
    expect(i32(I32_MIN).value).toBe(I32_MIN)
    expect(i32(I32_MAX).value).toBe(I32_MAX)
    expect(i32(-42).value).toBe(-42)
    expect(i32(0n).value).toBe(0)
  })

  it("rejects values outside the range, naming the type that fits", () => {
    expect(() => i32(I32_MAX + 1)).toThrow(InvalidValueError)
    expect(() => i32(Date.now())).toThrow(/use u64/)
    expect(() => i32(I32_MIN - 1)).toThrow(/use dec/)
    expect(() => i32(2n ** 40n)).toThrow(InvalidValueError)
  })

  it("rejects non-integers, quoting the dec call that would work", () => {
    expect(() => i32(3.5)).toThrow(/dec\("3\.5"\)/)
    expect(() => i32(Number.NaN)).toThrow(InvalidValueError)
    expect(() => i32(Number.POSITIVE_INFINITY)).toThrow(InvalidValueError)
  })
})

describe("u64", () => {
  it("accepts bigints, safe numbers, and decimal or hex strings", () => {
    expect(u64(0n).value).toBe(0n)
    expect(u64(U64_MAX).value).toBe(U64_MAX)
    expect(u64(1_200_000).value).toBe(1_200_000n)
    expect(u64("1200000").value).toBe(1_200_000n)
    expect(u64("0x124f80").value).toBe(1_200_000n)
  })

  it("rejects negatives, overflow, and lossy numbers", () => {
    expect(() => u64(-1n)).toThrow(/unsigned/)
    expect(() => u64(U64_MAX + 1n)).toThrow(/wider than 64 bits/)
    expect(() => u64(2 ** 53)).toThrow(/safe integer/)
    expect(() => u64(1.5)).toThrow(/dec\("1\.5"\)/)
    expect(() => u64("12abc")).toThrow(InvalidValueError)
  })

  it("points at u256 when the value is simply too wide", () => {
    expect(() => u64(U64_MAX + 1n)).toThrow(/Use u256/)
  })

  it("is a distinct type from u256, not an alias for a small one", () => {
    expect(TYPE_IDS[u64(1n).type]).toBe(3)
    expect(TYPE_IDS[u256(1n).type]).toBe(4)
  })
})

describe("u256", () => {
  it("accepts bigints, safe numbers, and decimal or hex strings", () => {
    expect(u256(0n).value).toBe(0n)
    expect(u256(U256_MAX).value).toBe(U256_MAX)
    expect(u256(1_000_000).value).toBe(1_000_000n)
    expect(u256("1000000").value).toBe(1_000_000n)
    expect(u256("0xf4240").value).toBe(1_000_000n)
  })

  it("rejects negatives, overflow, and lossy numbers", () => {
    expect(() => u256(-1n)).toThrow(/unsigned/)
    expect(() => u256(U256_MAX + 1n)).toThrow(/wider than 256 bits/)
    expect(() => u256(2 ** 53)).toThrow(/safe integer/)
    expect(() => u256(1.5)).toThrow(/dec\("1\.5"\)/)
    expect(() => u256("12abc")).toThrow(InvalidValueError)
  })
})

describe("dec", () => {
  it("canonicalises the decimal it was given", () => {
    expect(dec("3.5").value).toBe("3.5")
    expect(dec("3.50").value).toBe("3.5")
    expect(dec("003.5").value).toBe("3.5")
    expect(dec("+3.5").value).toBe("3.5")
    expect(dec("-0.0").value).toBe("0")
    expect(dec("5").value).toBe("5")
    expect(dec(5).value).toBe("5")
    expect(dec(-5n).value).toBe("-5")
  })

  it("holds exactly 18 decimal places", () => {
    expect(dec("0.000000000000000001").value).toBe("0.000000000000000001")
    expect(decUnits(dec("0.000000000000000001"))).toBe(1n)
    expect(decUnits(dec("3.5"))).toBe(3_500_000_000_000_000_000n)
    expect(decUnits(dec("-3.5"))).toBe(-3_500_000_000_000_000_000n)
  })

  it("errors rather than rounding away excess precision", () => {
    expect(() => dec("0.0000000000000000001")).toThrow(/19 fractional digits/)
    expect(() => dec("1e18")).toThrow(/not a decimal literal/)
    expect(() => dec("3.")).toThrow(/not a decimal literal/)
    expect(() => dec(".5")).toThrow(/not a decimal literal/)
  })

  it("refuses fractional numbers, since binary floats cannot hold them", () => {
    expect(() => dec(3.5)).toThrow(/dec\("3\.5"\)/)
    expect(() => dec(0.1 + 0.2)).toThrow(/dec\("0\.30000000000000004"\)/)
  })

  it("round-trips through its scaled units", () => {
    for (const literal of ["0", "1", "-1", "3.5", "-0.25", "123456789.000000000000000001"]) {
      expect(decFromUnits(decUnits(dec(literal))).value).toBe(dec(literal).value)
    }
  })
})

describe("str", () => {
  it("measures its 128-byte limit in bytes, not characters", () => {
    expect(str("a".repeat(128)).value).toHaveLength(128)
    expect(() => str("a".repeat(129))).toThrow(/129 UTF-8 bytes/)
    // "é" is two UTF-8 bytes, so 64 of them exactly fill the limit.
    expect(str("é".repeat(64)).value).toHaveLength(64)
    expect(() => str("é".repeat(65))).toThrow(/130 UTF-8 bytes/)
  })

  it("allows any character, control characters included", () => {
    for (const value of ["a\nb", "a\tb", "\u0000", "\u007f", "😀"]) {
      expect(str(value).value).toBe(value)
    }
  })

  it("rejects a lone surrogate, which has no UTF-8 encoding", () => {
    expect(() => str("a\ud800b")).toThrow(/lone surrogate at index 1/)
    expect(() => str("\udc00")).toThrow(/lone surrogate at index 0/)
    expect(() => str("a\ud83d")).toThrow(/lone surrogate at index 1/)
  })
})

describe("addr", () => {
  it("normalises to EIP-55 regardless of the case it was given", () => {
    expect(addr(VITALIK).value).toBe(VITALIK)
    expect(addr(VITALIK.toLowerCase()).value).toBe(VITALIK)
    expect(addr(`0x${VITALIK.slice(2).toUpperCase()}`).value).toBe(VITALIK)
  })

  it("catches a mistyped mixed-case address via its checksum", () => {
    const mistyped = `0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96046`
    expect(() => addr(mistyped)).toThrow(/EIP-55 checksum/)
    expect(() => addr(mistyped)).toThrow(/correctly checksummed form is 0x/)
  })

  it("rejects anything that is not 20 hex bytes", () => {
    expect(() => addr("0x1234")).toThrow(InvalidValueError)
    expect(() => addr(KEY)).toThrow(InvalidValueError)
  })
})

describe("key", () => {
  it("accept hex or raw bytes and normalise to lowercase hex", () => {
    expect(key(`0x${"AB".repeat(32)}`).value).toBe(KEY)
    expect(key(new Uint8Array(32).fill(0xab)).value).toBe(KEY)
  })

  it("reject anything that is not exactly 32 bytes", () => {
    expect(() => key(`0x${"ab".repeat(31)}`)).toThrow(/31 bytes/)
    expect(() => key(new Uint8Array(31))).toThrow(/not exactly 32 bytes/)
    expect(() => key("not hex" as `0x${string}`)).toThrow(/not 0x-prefixed hex/)
  })
})

describe("bytes", () => {
  it("accepts raw bytes or hex, and holds lowercase hex", () => {
    expect(bytes(new Uint8Array([0xde, 0xad])).value).toBe("0xdead")
    expect(bytes("0xDEAD").value).toBe("0xdead")
    expect(bytes(new Uint8Array()).value).toBe("0x")
  })

  it("takes hex as the bytes it spells, never as text", () => {
    expect(bytes("0xab").value).toBe("0xab")
  })

  it("rejects text, odd-length hex, and anything over 128 KiB", () => {
    expect(() => bytes("hello" as `0x${string}`)).toThrow(/not 0x-prefixed hex/)
    expect(() => bytes("0xabc")).toThrow(/odd number of hex digits/)
    expect(bytes(new Uint8Array(MAX_BYTES_LENGTH)).type).toBe("bytes")
    expect(() => bytes(new Uint8Array(MAX_BYTES_LENGTH + 1))).toThrow(/131073 bytes exceeds/)
  })
})

describe("dual-mode constructors", () => {
  const constructors = { bool, i32, u64, u256, dec, bytes, str, addr, key } as const

  it("describe a column when called with no arguments", () => {
    for (const [tag, construct] of Object.entries(constructors)) {
      const column = (construct as () => unknown)()
      expect(isColumn(column)).toBe(true)
      expect(column).toMatchObject({ type: tag, isIndexed: false, isOptional: false })
    }
  })

  it("throw for an explicit undefined rather than silently making a column", () => {
    for (const construct of Object.values(constructors)) {
      expect(() => (construct as (value: unknown) => unknown)(undefined)).toThrow(InvalidValueError)
    }
  })

  it("make a value, not a column, when given one", () => {
    expect(isColumn(dec("4.1"))).toBe(false)
    expect(dec("4.1")).toEqual(dec("4.10"))
  })
})

describe("toValue", () => {
  it("gives each bare form its unambiguous default type", () => {
    expect(toValue(true)).toEqual(bool(true))
    expect(toValue(10)).toEqual(i32(10))
    expect(toValue(10n)).toEqual(u256(10n))
    expect(toValue("hello")).toEqual(str("hello"))
    expect(toValue(new Uint8Array([1]))).toEqual(bytes("0x01"))
  })

  it("rejects a column where a value belongs, saying which call was meant", () => {
    expect(() => toValue(dec() as unknown as string)).toThrow(/a column, not a value/)
    expect(() => toValue(dec() as unknown as string)).toThrow(/dec\(\.\.\.\) with an argument/)
  })

  it("leaves a hex-looking string a string, rather than guessing at a key", () => {
    expect(toValue(KEY)).toEqual(str(KEY))
  })

  it("leaves a tagged value's type and value alone", () => {
    for (const tagged of [
      dec("3.5"),
      i32(-42),
      u256(10n),
      str("hi"),
      bool(true),
      key(KEY),
      bytes(KEY),
    ]) {
      expect(toValue(tagged)).toEqual(tagged)
    }
  })

  it("re-validates a tagged value rather than trusting its shape", () => {
    // The `validated` brand is type-level only, so nothing at runtime distinguishes a real u256
    // from an object that looks like one — out of JSON.parse, a config file, or an `as any`. Left
    // unchecked this was silent corruption, not an error: the ABI encoder would take the *string*
    // "1000" and write its UTF-8 bytes into the word, storing 2.27e76 on chain.
    const lookAlike = { type: "u256", value: "1000" } as unknown as string
    expect(toValue(lookAlike)).toEqual(u256(1000n))
    expect(toValue(lookAlike).value).toBe(1000n)

    // And a look-alike carrying something its type cannot hold is now rejected outright.
    expect(() => toValue({ type: "i32", value: 2 ** 40 } as unknown as string)).toThrow(
      InvalidValueError,
    )
    expect(() => toValue({ type: "addr", value: "0x1234" } as unknown as string)).toThrow(
      InvalidValueError,
    )
    expect(() => toValue({ type: "str", value: 42 } as unknown as string)).toThrow(
      InvalidValueError,
    )
  })

  it("rejects values it cannot type", () => {
    expect(() => toValue({ type: "nope", value: 1 } as unknown as string)).toThrow(
      UntypedValueError,
    )
    expect(() => toValue(new ArrayBuffer(32) as unknown as string)).toThrow(UntypedValueError)
    // `in` would have walked the prototype chain and accepted these as type tags.
    for (const inherited of ["constructor", "toString", "valueOf", "hasOwnProperty"]) {
      expect(() => toValue({ type: inherited, value: 1 } as unknown as string)).toThrow(
        UntypedValueError,
      )
    }
  })

  it("rejects an absent value by telling you to omit the attribute, not to infer a type", () => {
    // `undefined` and `null` are not assignable to ValueInput, but they still reach here at
    // runtime — from a spread of an optional-shaped object, or from data that never saw a type.
    for (const absent of [undefined, null]) {
      expect(() => toValue(absent as unknown as string)).toThrow(MissingValueError)
      expect(() => toValue(absent as unknown as string, "score")).toThrow(/Leave "score" out/)
    }
    expect(() => toValue(undefined as unknown as string, "score")).toThrow(
      /attribute "score" is undefined/,
    )
  })
})

describe("the validated brand", () => {
  it("makes a hand-written value object a compile error", () => {
    // @ts-expect-error — only the tagged constructors produce a validated value, which is what
    // guarantees every value on the wire has been range- and format-checked.
    const forged: ReturnType<typeof i32> = { type: "i32", value: 3.5 }
    expect(forged.value).toBe(3.5)
  })
})
