import { describe, expect } from "bun:test"
import { Effect, FileSystem, Option } from "effect"
import { join } from "node:path"
import { write, type Output } from "../src"
import { it } from "./effect"

describe("HttpApiCodegen.write", () => {
  it.effect("writes compiled files beneath the output directory", () => {
    const writes: Array<{ readonly path: string; readonly content: string }> = []
    const output: Output = {
      operations: [],
      files: [{ path: "session.ts", content: "export const session = {}" }],
    }

    return Effect.gen(function* () {
      yield* write(output, "/generated")

      // Paths are written with the OS-native separator (write() uses node:path join);
      // build the expected paths the same way so the assertion holds on every platform.
      expect(writes).toEqual([
        { path: join("/generated", "session.ts"), content: "export const session = {}\n" },
        { path: join("/generated", ".httpapi-codegen.json"), content: '[\n  "session.ts"\n]\n' },
      ])
    }).pipe(
      Effect.provideService(
        FileSystem.FileSystem,
        FileSystem.makeNoop({
          exists: () => Effect.succeed(false),
          makeDirectory: () => Effect.void,
          writeFileString: (path, content) => {
            writes.push({ path, content })
            return Effect.void
          },
        }),
      ),
    )
  })

  it.effect("removes only stale files owned by the previous manifest", () => {
    const removed: Array<string> = []
    return write(
      {
        operations: [],
        files: [{ path: "session.ts", content: "" }],
      },
      "/generated",
    ).pipe(
      Effect.provideService(
        FileSystem.FileSystem,
        FileSystem.makeNoop({
          exists: (path) => Effect.succeed(path.endsWith(".httpapi-codegen.json")),
          makeDirectory: () => Effect.void,
          readFileString: () => Effect.succeed('["old.ts", "session.ts"]'),
          remove: (path) => {
            removed.push(path)
            return Effect.void
          },
          writeFileString: () => Effect.void,
        }),
      ),
      Effect.tap(() => Effect.sync(() => expect(removed).toEqual([join("/generated", "old.ts")]))),
    )
  })

  it.effect("rejects unsafe and duplicate output paths before writing", () => {
    const writes: Array<string> = []
    return Effect.gen(function* () {
      const error = yield* write(
        {
          operations: [],
          files: [
            { path: "../outside.ts", content: "" },
            { path: "client.ts", content: "" },
            { path: "CLIENT.ts", content: "" },
          ],
        },
        "/generated",
      ).pipe(Effect.flip)

      expect(error._tag).toBe("GenerationError")
      expect(writes).toEqual([])
    }).pipe(
      Effect.provideService(
        FileSystem.FileSystem,
        FileSystem.makeNoop({
          writeFileString: (path) => {
            writes.push(path)
            return Effect.void
          },
        }),
      ),
    )
  })

  it.effect("rejects case-insensitive duplicate output paths", () => {
    const writes: Array<string> = []
    return Effect.gen(function* () {
      const error = yield* write(
        {
          operations: [],
          files: [
            { path: "client.ts", content: "" },
            { path: "CLIENT.ts", content: "" },
          ],
        },
        "/generated",
      ).pipe(Effect.flip)

      expect(error._tag).toBe("GenerationError")
      expect(error.reason).toBe("Duplicate output path: CLIENT.ts")
      expect(writes).toEqual([])
    }).pipe(
      Effect.provideService(
        FileSystem.FileSystem,
        FileSystem.makeNoop({
          writeFileString: (path) => {
            writes.push(path)
            return Effect.void
          },
        }),
      ),
    )
  })

  it.effect("reserves the private manifest path", () =>
    write({ operations: [], files: [{ path: ".httpapi-codegen.json", content: "" }] }, "/generated").pipe(
      Effect.flip,
      Effect.tap((error) => Effect.sync(() => expect(error.reason).toContain("Unsafe output path"))),
      Effect.provideService(FileSystem.FileSystem, FileSystem.makeNoop({})),
    ),
  )

  it.effect("rejects existing symbolic-link output targets", () =>
    write({ operations: [], files: [{ path: "session.ts", content: "" }] }, "/generated").pipe(
      Effect.flip,
      Effect.tap((error) => Effect.sync(() => expect(error.reason).toBe("Unsafe output path: session.ts"))),
      Effect.provideService(
        FileSystem.FileSystem,
        FileSystem.makeNoop({
          exists: (path) => Effect.succeed(path.endsWith("session.ts")),
          makeDirectory: () => Effect.void,
          stat: () =>
            Effect.succeed({
              type: "SymbolicLink",
              mtime: Option.none(),
              atime: Option.none(),
              birthtime: Option.none(),
              dev: 0,
              ino: Option.none(),
              mode: 0,
              nlink: Option.none(),
              uid: Option.none(),
              gid: Option.none(),
              rdev: Option.none(),
              size: FileSystem.Size(0),
              blksize: Option.none(),
              blocks: Option.none(),
            }),
        }),
      ),
    ),
  )
})
