import { describe, expect, it } from "bun:test"
import { decodeAbiParameters, parseAbiParameters, toHex } from "viem"
import { encodeCreate } from "../schema/attributes"
import { defineSchema } from "../schema/schema"
import { bytes, dec, i32, str, u256 } from "../schema/values"
import { InvalidCreationFlagsError } from "./errors"
import { decodeCreationFlags, encodeCreationFlags } from "./flags"
import { MAX_SALT, NO_SALT, predictEntityKey, randomSalt, resolveSalt, validateSalt } from "./key"
import { createOperation } from "./operations"
import { OperationType } from "./params"

const OWNER = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"

const CREATE_PARAMS = parseAbiParameters(
  "(uint128 salt, uint64 expiresAt, uint64 minLifetime, uint8 creationFlags, (bytes32 name, uint8 typeId, bytes value)[] attributes)",
)

/** Decodes an operation's payload back into the Create struct it encodes. */
function decodeCreate(operationData: `0x${string}`) {
  const [create] = decodeAbiParameters(CREATE_PARAMS, operationData)
  return create
}

describe("creation flags", () => {
  it("packs each flag into its documented bit", () => {
    expect(encodeCreationFlags({})).toBe(0b00)
    expect(encodeCreationFlags({ readonly: true })).toBe(0b01)
    expect(encodeCreationFlags({ permissionlessExtension: true })).toBe(0b10)
    expect(encodeCreationFlags({ readonly: true, permissionlessExtension: true })).toBe(0b11)
    expect(encodeCreationFlags()).toBe(0)
  })

  it("round-trips through the byte", () => {
    for (const readonly of [true, false]) {
      for (const permissionlessExtension of [true, false]) {
        const decoded = decodeCreationFlags(
          encodeCreationFlags({ readonly, permissionlessExtension }),
        )
        expect(decoded.readonly).toBe(readonly)
        expect(decoded.permissionlessExtension).toBe(permissionlessExtension)
      }
    }
  })

  it("refuses a value that is not a byte at all", () => {
    // Malformed input
    expect(() => decodeCreationFlags(256)).toThrow(InvalidCreationFlagsError)
    expect(() => decodeCreationFlags(256)).toThrow(/not a byte/)
    expect(() => decodeCreationFlags(-1)).toThrow(/not a byte/)
  })

  it("keeps the raw byte, so unknown state stays visible", () => {
    expect(decodeCreationFlags(0b11).raw).toBe(3)
  })
})

describe("salt", () => {
  it("generates 128 bits within the wire field", () => {
    for (let i = 0; i < 32; i++) {
      const salt = randomSalt()
      expect(salt).toBeGreaterThanOrEqual(0n)
      expect(salt).toBeLessThanOrEqual(MAX_SALT)
    }
  })

  it("does not repeat", () => {
    const seen = new Set(Array.from({ length: 64 }, () => randomSalt().toString()))
    expect(seen.size).toBe(64)
  })

  it("bounds a caller-supplied salt to the uint128 field", () => {
    expect(validateSalt(0n)).toBe(0n)
    expect(validateSalt(MAX_SALT)).toBe(MAX_SALT)
    expect(() => validateSalt(MAX_SALT + 1n)).toThrow(/uint128/)
    expect(() => validateSalt(-1n)).toThrow(/uint128/)
  })

  it("resolves NO_SALT to the zero, and passes anything else through", () => {
    expect(resolveSalt(NO_SALT)).toBe(0n)
    expect(resolveSalt(0n)).toBe(0n)
    expect(resolveSalt(42n)).toBe(42n)
    expect(() => resolveSalt(MAX_SALT + 1n)).toThrow(/uint128/)
  })

  it("reports a salt it cannot use without choking on a symbol", () => {
    // @ts-expect-error
    expect(() => validateSalt(Symbol("nope"))).toThrow(/Invalid salt/)
  })
})

describe("predictEntityKey", () => {
  it("is deterministic in owner, nonce and salt", () => {
    const base = { owner: OWNER, nonce: 0n, salt: 7n, chainId: 1 } as const
    expect(predictEntityKey(base)).toBe(predictEntityKey(base))
    expect(predictEntityKey({ ...base, nonce: 1n })).not.toBe(predictEntityKey(base))
    expect(predictEntityKey({ ...base, salt: 8n })).not.toBe(predictEntityKey(base))
    expect(
      predictEntityKey({ ...base, owner: "0x1111111111111111111111111111111111111111" }),
    ).not.toBe(predictEntityKey(base))
  })

  it("derives the same key from NO_SALT as from an explicit zero", () => {
    const base = { owner: OWNER, nonce: 3n, chainId: 1 } as const
    expect(predictEntityKey({ ...base, salt: NO_SALT })).toBe(
      predictEntityKey({ ...base, salt: 0n }),
    )
  })

  it("separates chains through the domain", () => {
    const base = { owner: OWNER, nonce: 0n, salt: 0n } as const
    expect(predictEntityKey({ ...base, chainId: 1 })).not.toBe(
      predictEntityKey({ ...base, chainId: 2 }),
    )
  })

  it("returns a 32-byte key", () => {
    expect(predictEntityKey({ owner: OWNER, nonce: 0n, salt: 0n, chainId: 1 })).toMatch(
      /^0x[0-9a-f]{64}$/,
    )
  })
})

describe("the Create operation", () => {
  const build = (over: Partial<Parameters<typeof createOperation>[0]> = {}) =>
    createOperation({
      salt: 42n,
      expiry: { expiresAt: 0n, minLifetime: 43_200n },
      creationFlags: 0,
      attributes: [],
      ...over,
    })

  it("carries the CREATE tag", () => {
    expect(build().operation).toBe(OperationType.Create)
  })

  it("round-trips its fields through the ABI", () => {
    const create = decodeCreate(
      build({
        salt: 42n,
        expiry: { expiresAt: 1_200_000n, minLifetime: 100n },
        creationFlags: 0b11,
      }).operationData,
    )
    expect(create.salt).toBe(42n)
    expect(create.expiresAt).toBe(1_200_000n)
    expect(create.minLifetime).toBe(100n)
    expect(create.creationFlags).toBe(3)
  })

  it("carries attributes as (name, typeId, bytes), sorted ascending by name", () => {
    const schema = defineSchema({ zebra: i32(), apple: str(), mango: u256() })
    const create = decodeCreate(
      build({
        attributes: encodeCreate(schema, { zebra: i32(1), apple: str("x"), mango: u256(2n) }),
      }).operationData,
    )
    const names = create.attributes.map((a) => a.name)
    expect(names).toEqual([...names].sort())
    expect(create.attributes.map((a) => a.typeId)).toEqual([8, 4, 2]) // apple str, mango u256, zebra i32
  })

  it("sets bit 7 of the typeId on indexed columns, and only on them", () => {
    const schema = defineSchema({ level: i32().indexed(), note: str(), body: bytes() })
    const create = decodeCreate(
      build({
        attributes: encodeCreate(schema, {
          level: 10,
          note: "hi",
          body: new Uint8Array([0xde, 0xad]),
        }),
      }).operationData,
    )
    const byName = Object.fromEntries(create.attributes.map((a) => [a.name, a]))
    expect(byName[toHex("level", { size: 32 })].typeId).toBe(0x80 | 2)
    expect(byName[toHex("note", { size: 32 })].typeId).toBe(8)
    expect(byName[toHex("body", { size: 32 })]).toMatchObject({ typeId: 7, value: "0xdead" })
  })

  it("carries no cells at all for an entity with no columns", () => {
    const create = decodeCreate(
      build({ attributes: encodeCreate(defineSchema({}), {}) }).operationData,
    )
    expect(create.attributes).toHaveLength(0)
  })

  it("encodes each value type at its documented width", () => {
    const schema = defineSchema({ n: i32(), big: u256(), d: dec(), s: str() })
    const create = decodeCreate(
      build({
        attributes: encodeCreate(schema, {
          n: i32(-42),
          big: u256(1n),
          d: dec("1.5"),
          s: str("hi"),
        }),
      }).operationData,
    )
    const byName = Object.fromEntries(create.attributes.map((a) => [a.name, a.value]))
    // Word types are exactly 32 bytes; i32 is sign-extended.
    expect(byName[toHex("n", { size: 32 })]).toBe(`0x${"ff".repeat(31)}d6`)
    expect(byName[toHex("big", { size: 32 })]).toBe(`0x${"00".repeat(31)}01`)
    expect(BigInt(byName[toHex("d", { size: 32 })])).toBe(1_500_000_000_000_000_000n)
    // str is raw UTF-8, not padded.
    expect(byName[toHex("s", { size: 32 })]).toBe("0x6869")
  })
})
