import { describe, it, expect, beforeEach, afterEach, vi, beforeAll, afterAll } from "vitest"
import { writeFileSync, unlinkSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { sendMessage, sendDocument, sendPhoto } from "../src/api.ts"

const TOKEN = "TESTOKEN"
const CHAT = "123"

const TMP_DOC = join(tmpdir(), "tg-test-doc.txt")
const TMP_PHOTO = join(tmpdir(), "tg-test-photo.png")

function okResponse(result: unknown) {
  const payload = { ok: true as const, result }
  return { ...payload, json: async () => payload }
}

beforeAll(() => {
  writeFileSync(TMP_DOC, "hello document")
  writeFileSync(TMP_PHOTO, "fake-png-bytes")
})

afterAll(() => {
  unlinkSync(TMP_DOC)
  unlinkSync(TMP_PHOTO)
})

beforeEach(() => {
  vi.stubGlobal("fetch", vi.fn())
})

afterEach(() => {
  vi.unstubAllGlobals()
})

describe("sendMessage", () => {
  it("POSTs to sendMessage with chat_id, text; parse_mode omitted when undefined", async () => {
    vi.mocked(fetch).mockResolvedValue(okResponse({ message_id: 42, chat: { id: 999 } }) as never)
    await sendMessage(TOKEN, CHAT, "hi")
    const [url, opts] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit]
    expect(url).toBe(`https://api.telegram.org/bot${TOKEN}/sendMessage`)
    expect(opts.method).toBe("POST")
    expect((opts.headers as Record<string, string>)["Content-Type"]).toBe("application/json")
    const body = JSON.parse(opts.body as string)
    expect(body).toEqual({ chat_id: CHAT, text: "hi" })
  })

  it("POSTs to sendMessage with parse_mode=MarkdownV2 when explicitly passed", async () => {
    vi.mocked(fetch).mockResolvedValue(okResponse({ message_id: 1, chat: { id: 1 } }) as never)
    await sendMessage(TOKEN, CHAT, "hi", "MarkdownV2")
    const [, opts] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit]
    const body = JSON.parse(opts.body as string)
    expect(body).toEqual({ chat_id: CHAT, text: "hi", parse_mode: "MarkdownV2" })
  })

  it("parseMode='plain' omits parse_mode from body", async () => {
    vi.mocked(fetch).mockResolvedValue(okResponse({ message_id: 1, chat: { id: 1 } }) as never)
    await sendMessage(TOKEN, CHAT, "hi", "plain")
    const [, opts] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit]
    const body = JSON.parse(opts.body as string)
    expect(body).toEqual({ chat_id: CHAT, text: "hi" })
    expect(body).not.toHaveProperty("parse_mode")
  })

  it("parseMode='HTML' sets parse_mode=HTML", async () => {
    vi.mocked(fetch).mockResolvedValue(okResponse({ message_id: 1, chat: { id: 1 } }) as never)
    await sendMessage(TOKEN, CHAT, "hi", "HTML")
    const [, opts] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit]
    const body = JSON.parse(opts.body as string)
    expect(body.parse_mode).toBe("HTML")
  })

  it("returns {ok, message_id, chat_id} on success", async () => {
    vi.mocked(fetch).mockResolvedValue(okResponse({ message_id: 42, chat: { id: 999 } }) as never)
    const r = await sendMessage(TOKEN, CHAT, "hi")
    expect(r).toEqual({ ok: true, message_id: 42, chat_id: "999" })
  })

  it("throws on Telegram API error (ok:false)", async () => {
    const errPayload = { ok: false, description: "Unauthorized" }
    vi.mocked(fetch).mockResolvedValue({ ...errPayload, json: async () => errPayload } as never)
    await expect(sendMessage(TOKEN, CHAT, "hi")).rejects.toThrow(/Unauthorized/)
  })

  it("throws on network error (fetch rejects)", async () => {
    vi.mocked(fetch).mockRejectedValue(new Error("network"))
    await expect(sendMessage(TOKEN, CHAT, "hi")).rejects.toThrow(/network/)
  })
})

describe("sendDocument", () => {
  it("POSTs to sendDocument with FormData (chat_id, document, caption)", async () => {
    vi.mocked(fetch).mockResolvedValue(okResponse({ message_id: 7, chat: { id: CHAT } }) as never)
    const appendSpy = vi.spyOn(FormData.prototype, "append")
    await sendDocument(TOKEN, CHAT, TMP_DOC, "cap", "MarkdownV2")
    const [url, opts] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit]
    expect(url).toBe(`https://api.telegram.org/bot${TOKEN}/sendDocument`)
    expect(opts.method).toBe("POST")
    const form = opts.body as FormData
    expect(form.get("chat_id")).toBe(CHAT)
    expect(form.get("caption")).toBe("cap")
    expect(form.get("parse_mode")).toBe("MarkdownV2")
    const doc = form.get("document")
    expect(doc).toBeInstanceOf(Blob)
    expect((doc as File).name).toBe("tg-test-doc.txt")
    expect(appendSpy).toHaveBeenCalledWith("chat_id", CHAT)
    expect(appendSpy).toHaveBeenCalledWith("document", expect.any(Blob), "tg-test-doc.txt")
    appendSpy.mockRestore()
  })
})

describe("sendPhoto", () => {
  it("POSTs to sendPhoto with FormData (chat_id, photo field)", async () => {
    vi.mocked(fetch).mockResolvedValue(okResponse({ message_id: 9, chat: { id: CHAT } }) as never)
    const appendSpy = vi.spyOn(FormData.prototype, "append")
    await sendPhoto(TOKEN, CHAT, TMP_PHOTO, "cover", "HTML")
    const [url, opts] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit]
    expect(url).toBe(`https://api.telegram.org/bot${TOKEN}/sendPhoto`)
    expect(opts.method).toBe("POST")
    const form = opts.body as FormData
    expect(form.get("chat_id")).toBe(CHAT)
    expect(form.get("caption")).toBe("cover")
    expect(form.get("parse_mode")).toBe("HTML")
    const photo = form.get("photo")
    expect(photo).toBeInstanceOf(Blob)
    expect((photo as File).name).toBe("tg-test-photo.png")
    expect(appendSpy).toHaveBeenCalledWith("photo", expect.any(Blob), "tg-test-photo.png")
    appendSpy.mockRestore()
  })
})