import { describe, expect, test } from "bun:test"
import { AppRuntime } from "../../src/effect/app-runtime"
import { Effect } from "effect"
import { Instance } from "../../src/project/instance"
import { Pty } from "../../src/pty"
import { tmpdir } from "../fixture/fixture"
import { setTimeout as sleep } from "node:timers/promises"

describe("pty", () => {
  test("does not leak output when websocket objects are reused", async () => {
    await using dir = await tmpdir({ git: true })

    await Instance.provide({
      directory: dir.path,
      fn: () =>
        AppRuntime.runPromise(
          Effect.gen(function* () {
            const pty = yield* Pty.Service
            const a = yield* pty.create({ command: "cat", title: "a" })
            const b = yield* pty.create({ command: "cat", title: "b" })
            try {
              const outA: string[] = []
              const outB: string[] = []

              const ws = {
                readyState: 1,
                data: { events: { connection: "a" } },
                send: (data: unknown) => {
                  outA.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
                },
                close: () => {
                  // no-op (simulate abrupt drop)
                },
              }

              yield* pty.connect(a.id, ws as any)

              ws.data = { events: { connection: "b" } }
              ws.send = (data: unknown) => {
                outB.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
              }
              yield* pty.connect(b.id, ws as any)

              outA.length = 0
              outB.length = 0

              yield* pty.write(a.id, "AAA\n")
              yield* Effect.promise(() => sleep(100))

              expect(outB.join("")).not.toContain("AAA")
            } finally {
              yield* pty.remove(a.id)
              yield* pty.remove(b.id)
            }
          }),
        ),
    })
  })

  test("does not leak output when Bun recycles websocket objects before re-connect", async () => {
    await using dir = await tmpdir({ git: true })

    await Instance.provide({
      directory: dir.path,
      fn: () =>
        AppRuntime.runPromise(
          Effect.gen(function* () {
            const pty = yield* Pty.Service
            const a = yield* pty.create({ command: "cat", title: "a" })
            try {
              const outA: string[] = []
              const outB: string[] = []

              const ws = {
                readyState: 1,
                data: { events: { connection: "a" } },
                send: (data: unknown) => {
                  outA.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
                },
                close: () => {
                  // no-op (simulate abrupt drop)
                },
              }

              yield* pty.connect(a.id, ws as any)
              outA.length = 0

              ws.data = { events: { connection: "b" } }
              ws.send = (data: unknown) => {
                outB.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
              }

              yield* pty.write(a.id, "AAA\n")
              yield* Effect.promise(() => sleep(100))

              expect(outB.join("")).not.toContain("AAA")
            } finally {
              yield* pty.remove(a.id)
            }
          }),
        ),
    })
  })

  test("treats in-place socket data mutation as the same connection", async () => {
    await using dir = await tmpdir({ git: true })

    await Instance.provide({
      directory: dir.path,
      fn: () =>
        AppRuntime.runPromise(
          Effect.gen(function* () {
            const pty = yield* Pty.Service
            const a = yield* pty.create({ command: "cat", title: "a" })
            try {
              const out: string[] = []

              const ctx = { connId: 1 }
              const ws = {
                readyState: 1,
                data: ctx,
                send: (data: unknown) => {
                  out.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
                },
                close: () => {
                  // no-op
                },
              }

              yield* pty.connect(a.id, ws as any)
              out.length = 0

              ctx.connId = 2

              yield* pty.write(a.id, "AAA\n")
              yield* Effect.promise(() => sleep(100))

              expect(out.join("")).toContain("AAA")
            } finally {
              yield* pty.remove(a.id)
            }
          }),
        ),
    })
  })
})
