import { describe, expect, test } from "bun:test"

// ── Boxes atoms integrated into saeeol ──

// 1. disposable → util/defer
import { defer } from "@/util/defer"

// 2. cancel → util/abort
import { abortAfter, abortAfterAny } from "@/util/abort"

// 3. human/chop/plural → util/locale
import { Locale } from "@/util/locale"

// 4. human.secsDuration → util/format
import { formatDuration } from "@/util/format"

// 5. err → util/error
import { errorMessage, errorFormat } from "@/util/error"

// 6. rwlock → util/lock
import * as Lock from "@/util/lock"

// 7. clamp
import { clamp } from "@/boxes/clamp"

// 8. schedule (expDelay)
import { expDelay } from "@/boxes/schedule"

// 9. retry
import { retry } from "@/boxes/retry"

// 10. b64
import { b64EncBytes } from "@/boxes/b64"

// 11. ttl-cache
import { TTLCache } from "@/boxes/ttl-cache"

// 12. bom → util/bom
import * as Bom from "@/util/bom"

// 13. scope → util/local-context
import * as LocalContext from "@/util/local-context"

// 14. dataurl
import * as DataUrl from "@/boxes/dataurl"

// 15. puny
import { normalizeUrls } from "@/saeeol/util/url"

// 16. ansi → util/color
import * as Color from "@/util/color"

// 17. plural
import { plural } from "@/boxes/plural"

describe("boxes integration: defer (disposable)", () => {
  test("defer wraps cleanup as Disposable", () => {
    let cleaned = false
    const d = defer(() => { cleaned = true })
    d[Symbol.dispose]()
    expect(cleaned).toBe(true)
  })

  test("defer supports async dispose", async () => {
    let cleaned = false
    const d = defer(() => { cleaned = true })
    await d[Symbol.asyncDispose]()
    expect(cleaned).toBe(true)
  })
})

describe("boxes integration: abort (cancel)", () => {
  test("abortAfter creates signal", () => {
    const { signal, clearTimeout } = abortAfter(5000)
    expect(signal.aborted).toBe(false)
    clearTimeout()
  })

  test("abortAfterAny combines signals", () => {
    const { signal, clearTimeout } = abortAfterAny(5000)
    expect(signal.aborted).toBe(false)
    clearTimeout()
  })
})

describe("boxes integration: locale (human/chop/plural)", () => {
  test("number formats compact", () => {
    expect(Locale.number(1500)).toBe("1.5K")
    expect(Locale.number(1_500_000)).toBe("1.5M")
  })

  test("duration formats ms", () => {
    expect(Locale.duration(500)).toBe("500ms")
    expect(Locale.duration(1500)).toBe("1.5s")
    expect(Locale.duration(90000)).toBe("1m 30s")
  })

  test("truncate chops with ellipsis", () => {
    expect(Locale.truncate("hello world", 20)).toBe("hello world")
    expect(Locale.truncate("hello world", 8)).toBe("hello w…")
  })

  test("truncateMiddle preserves start and end", () => {
    expect(Locale.truncateMiddle("abcdefghij", 7)).toContain("…")
  })

  test("pluralize handles singular/plural", () => {
    expect(Locale.pluralize(1, "{} item", "{} items")).toBe("1 item")
    expect(Locale.pluralize(5, "{} item", "{} items")).toBe("5 items")
  })
})

describe("boxes integration: formatDuration", () => {
  test("formats seconds", () => {
    expect(formatDuration(30)).toBe("30s")
    expect(formatDuration(90)).toBe("1m 30s")
    expect(formatDuration(3600)).toBe("1h")
  })
})

describe("boxes integration: error (err)", () => {
  test("errorMessage extracts message", () => {
    expect(errorMessage(new Error("test"))).toBe("test")
    expect(errorMessage({ message: "obj" })).toBe("obj")
  })

  test("errorFormat includes stack", () => {
    const err = new Error("boom")
    expect(errorFormat(err)).toContain("boom")
  })
})

describe("boxes integration: lock (rwlock)", () => {
  test("concurrent reads", async () => {
    const key = `test-rw-${Date.now()}`
    const r1 = await Lock.read(key)
    const r2 = await Lock.read(key)
    r1[Symbol.dispose]()
    r2[Symbol.dispose]()
  })

  test("write excludes readers", async () => {
    const key = `test-w-${Date.now()}`
    const w = await Lock.write(key)
    w[Symbol.dispose]()
  })
})

describe("boxes integration: clamp", () => {
  test("clamps value within range", () => {
    expect(clamp(5, 0, 10)).toBe(5)
    expect(clamp(-1, 0, 10)).toBe(0)
    expect(clamp(15, 0, 10)).toBe(10)
  })
})

describe("boxes integration: expDelay", () => {
  test("computes exponential delay", () => {
    expect(expDelay(1000, 0)).toBe(1000)
    expect(expDelay(1000, 1)).toBe(2000)
    expect(expDelay(1000, 2)).toBe(4000)
  })

  test("respects custom factor", () => {
    expect(expDelay(500, 0, 3)).toBe(500)
    expect(expDelay(500, 1, 3)).toBe(1500)
  })
})

describe("boxes integration: retry", () => {
  test("succeeds on first try", async () => {
    const result = await retry(() => Promise.resolve("ok"))
    expect(result).toBe("ok")
  })

  test("retries on failure", async () => {
    let attempts = 0
    const result = await retry(
      async () => { attempts++; if (attempts < 2) throw new Error("fail"); return "done" },
      { attempts: 3, delay: 1, retryIf: () => true },
    )
    expect(result).toBe("done")
    expect(attempts).toBe(2)
  })
})

describe("boxes integration: b64EncBytes", () => {
  test("encodes ArrayBuffer to base64url", () => {
    const buf = new TextEncoder().encode("hello").buffer
    const encoded = b64EncBytes(buf)
    expect(encoded).not.toContain("+")
    expect(encoded).not.toContain("/")
    expect(encoded).not.toContain("=")
  })
})

describe("boxes integration: TTLCache", () => {
  test("stores and retrieves values", () => {
    const cache = new TTLCache<string, number>({ defaultTtl: 60_000 })
    cache.set("a", 1)
    expect(cache.get("a")).toBe(1)
  })

  test("expires entries", () => {
    const cache = new TTLCache<string, number>({ defaultTtl: 1 })
    cache.set("a", 1)
    // Wait for expiry
    const start = Date.now()
    while (Date.now() - start < 5) {}
    expect(cache.get("a")).toBeUndefined()
  })
})

describe("boxes integration: Bom", () => {
  test("split detects BOM", () => {
    const result = Bom.split("\uFEFFhello")
    expect(result.bom).toBe(true)
    expect(result.text).toBe("hello")
  })

  test("split passes through no-BOM text", () => {
    const result = Bom.split("hello")
    expect(result.bom).toBe(false)
    expect(result.text).toBe("hello")
  })

  test("join adds/removes BOM", () => {
    expect(Bom.join("hello", true)).toBe("\uFEFFhello")
    expect(Bom.join("\uFEFFhello", false)).toBe("hello")
  })
})

describe("boxes integration: LocalContext (scope)", () => {
  test("provides and uses context", () => {
    const ctx = LocalContext.create<{ value: number }>("test")
    const result = ctx.provide({ value: 42 }, () => ctx.use().value)
    expect(result).toBe(42)
  })

  test("throws NotFound when no context", () => {
    const ctx = LocalContext.create<{ value: number }>("missing")
    expect(() => ctx.use()).toThrow(LocalContext.NotFound)
  })
})

describe("boxes integration: DataUrl", () => {
  test("decode parses base64 data URL", () => {
    expect(DataUrl.decode("data:text/plain;base64,aGVsbG8=")).toBe("hello")
  })

  test("encode creates data URL", () => {
    const result = DataUrl.encode("text/plain", "hello")
    expect(result).toContain("data:text/plain;base64,")
  })

  test("parse extracts mime and data", () => {
    const result = DataUrl.parse("data:text/plain;base64,aGVsbG8=")
    expect(result?.mime).toBe("text/plain")
    expect(result?.data).toBe("hello")
  })

  test("parse returns undefined for non-data URLs", () => {
    expect(DataUrl.parse("https://example.com")).toBeUndefined()
  })
})

describe("boxes integration: normalizeUrls (puny)", () => {
  test("normalizes IDN hostnames", () => {
    const result = normalizeUrls("https://аpitest.com/path")
    expect(result).toContain("xn--")
  })

  test("leaves ASCII URLs unchanged", () => {
    expect(normalizeUrls("https://example.com/path")).toBe("https://example.com/path")
  })
})

describe("boxes integration: Color (ansi)", () => {
  test("isValidHex validates hex colors", () => {
    expect(Color.isValidHex("#ff6600")).toBe(true)
    expect(Color.isValidHex("#abc")).toBe(false)
    expect(Color.isValidHex(undefined)).toBe(false)
  })

  test("hexToRgb parses hex", () => {
    const { r, g, b } = Color.hexToRgb("#ff6600")
    expect(r).toBe(255)
    expect(g).toBe(102)
    expect(b).toBe(0)
  })

  test("hexToAnsiBold produces escape sequence", () => {
    const result = Color.hexToAnsiBold("#ff6600")
    expect(result).toContain("\x1b[38;2;")
    expect(result).toContain("\x1b[1m")
  })

  test("hexToAnsiBold returns undefined for invalid", () => {
    expect(Color.hexToAnsiBold("invalid")).toBeUndefined()
  })
})

describe("boxes integration: plural", () => {
  test("returns singular for 1", () => {
    expect(plural(1, "{} file", "{} files")).toBe("1 file")
  })

  test("returns plural for 0 and 2+", () => {
    expect(plural(0, "{} file", "{} files")).toBe("0 files")
    expect(plural(5, "{} file", "{} files")).toBe("5 files")
  })
})

// ── Newly integrated atoms ──

// 18. uid.random
import { random as randomId, ascend, ts as uidTs } from "@/boxes/uid"

describe("boxes integration: uid", () => {
  test("random generates UUID v4", () => {
    const id = randomId()
    expect(id).toMatch(/^[0-9a-f-]{36}$/)
  })

  test("ascend generates sortable ID", () => {
    const id = ascend("test")
    expect(id).toMatch(/^test_/)
    expect(typeof uidTs(id)).toBe("number")
  })
})

// 19. atomic-write
import { atomicWrite } from "@/boxes/atomic-write"
import { mkdtemp, rm } from "fs/promises"
import { join } from "path"
import { tmpdir } from "os"

describe("boxes integration: atomicWrite", () => {
  test("writes file atomically", async () => {
    const dir = await mkdtemp(join(tmpdir(), "atomic-test-"))
    const filePath = join(dir, "test.txt")
    await atomicWrite(filePath, "hello world")
    const content = await Bun.file(filePath).text()
    expect(content).toBe("hello world")
    await rm(dir, { recursive: true })
  })

  test("creates parent dirs on ENOENT", async () => {
    const dir = await mkdtemp(join(tmpdir(), "atomic-test-"))
    const filePath = join(dir, "sub", "dir", "test.txt")
    await atomicWrite(filePath, "nested content")
    const content = await Bun.file(filePath).text()
    expect(content).toBe("nested content")
    await rm(dir, { recursive: true })
  })
})

// 20. compact (filterTruthy)
import { filterTruthy } from "@/boxes/compact"

describe("boxes integration: filterTruthy", () => {
  test("removes falsy values", () => {
    expect(filterTruthy([1, null, 2, undefined, 0, "", 3])).toEqual([1, 2, 3])
  })

  test("preserves all truthy", () => {
    expect(filterTruthy(["a", "b", "c"])).toEqual(["a", "b", "c"])
  })
})

// 21. typed-event
import { TypedEventBus } from "@/boxes/typed-event"

describe("boxes integration: TypedEventBus", () => {
  test("on/emit/off cycle", () => {
    const bus = new TypedEventBus<{ click: number }>()
    const received: number[] = []
    bus.on("click", (n) => received.push(n))
    bus.emit("click", 42)
    expect(received).toEqual([42])
  })

  test("off stops delivery", () => {
    const bus = new TypedEventBus<{ evt: string }>()
    const received: string[] = []
    const unsub = bus.on("evt", (s) => received.push(s))
    bus.emit("evt", "a")
    unsub()
    bus.emit("evt", "b")
    expect(received).toEqual(["a"])
  })

  test("setMaxListeners returns this", () => {
    const bus = new TypedEventBus()
    expect(bus.setMaxListeners(10)).toBe(bus)
  })
})

// 22. diff-apply
import { parseUnifiedDiff, applyUnifiedDiff } from "@/boxes/diff-apply"

describe("boxes integration: diff-apply", () => {
  test("parses and applies unified diff", () => {
    const source = "line1\nline2\nline3"
    const diff = "@@ -1,3 +1,3 @@\n line1\n-old\n+new\n line3"
    const result = applyUnifiedDiff(source, diff)
    expect(result).toBe("line1\nnew\nline3")
  })

  test("parseUnifiedDiff extracts hunks", () => {
    const hunks = parseUnifiedDiff("@@ -1,3 +1,3 @@\n-a\n+b")
    expect(hunks).toHaveLength(1)
    expect(hunks[0].oldStart).toBe(1)
  })
})
