import { afterEach, describe, expect, spyOn, test } from "bun:test"
import { CodeIndexManager } from "@saeeol/indexing/engine"
import type { Config } from "../../src/config/config"
import { GlobalBus } from "../../src/bus/global"
import { getBootstrapRunEffect } from "../../src/effect/app-runtime"
import { SaeeolIndexing } from "../../src/overlay/indexing"
import { Instance } from "../../src/project/instance"
import { Server } from "../../src/server/server"
import * as Log from "@saeeol/core/util/log"
import { disposeAllInstances, tmpdir } from "../fixture/fixture"

Log.init({ print: false })

const fetch = global.fetch

const cfg: Partial<Config.Info> = {
  plugin: ["@saeeol/indexing"],
  experimental: {
    semantic_indexing: true,
  },
  indexing: {
    enabled: true,
    provider: "ollama",
    vectorStore: "qdrant",
    ollama: {
      baseUrl: "http://127.0.0.1:1",
    },
  },
}

const off: Partial<Config.Info> = {
  plugin: ["@saeeol/indexing"],
  experimental: {
    semantic_indexing: false,
  },
  indexing: {
    enabled: true,
    provider: "ollama",
    vectorStore: "qdrant",
    ollama: {
      baseUrl: "http://127.0.0.1:1",
    },
  },
}
const saeeol: Partial<Config.Info> = {
  plugin: ["@saeeol/indexing"],
  experimental: {
    semantic_indexing: true,
  },
  indexing: {
    enabled: true,
    vectorStore: "qdrant",
  },
}
const implicitOpenAi: Partial<Config.Info> = {
  plugin: ["@saeeol/indexing"],
  experimental: {
    semantic_indexing: true,
  },
  indexing: {
    enabled: true,
    vectorStore: "qdrant",
    openai: {
      apiKey: "openai-token",
    },
  },
}
const configDir = process.env["SAEEOL_CONFIG_DIR"]
const disabled = process.env["SAEEOL_DISABLE_CODEBASE_INDEXING"]
const error = new Error("test indexing initialization failed")

async function wait(read: () => Promise<SaeeolIndexing.Status>, state: SaeeolIndexing.Status["state"]) {
  for (const _ of Array.from({ length: 100 })) {
    const status = await read()
    if (status.state === state) return status
    await new Promise((resolve) => setTimeout(resolve, 10))
  }
  throw new Error(`indexing did not reach ${state}`)
}

async function called(init: ReturnType<typeof spyOn<CodeIndexManager, "initialize">>) {
  for (const _ of Array.from({ length: 100 })) {
    if (init.mock.calls.length > 0) return
    await new Promise((resolve) => setTimeout(resolve, 10))
  }
  throw new Error("indexing initialization did not start")
}

afterEach(async () => {
  if (configDir === undefined) delete process.env["SAEEOL_CONFIG_DIR"]
  else process.env["SAEEOL_CONFIG_DIR"] = configDir
  if (disabled === undefined) delete process.env["SAEEOL_DISABLE_CODEBASE_INDEXING"]
  else process.env["SAEEOL_DISABLE_CODEBASE_INDEXING"] = disabled
  global.fetch = fetch
  await disposeAllInstances()
})

describe("indexing startup degradation", () => {
  test("keeps server routes alive when indexing initialization fails", async () => {
    const init = spyOn(CodeIndexManager.prototype, "initialize").mockRejectedValue(error)

    await using tmp = await tmpdir({ git: true, config: cfg })
    process.env["SAEEOL_CONFIG_DIR"] = tmp.path

    try {
      const app = Server.Default().app

      const config = await app.request("/config", {
        headers: {
          "x-saeeol-directory": tmp.path,
        },
      })
      expect(config.status).toBe(200)

      const body = await wait(async () => {
        const status = await app.request("/indexing/status", {
          headers: {
            "x-saeeol-directory": tmp.path,
          },
        })
        expect(status.status).toBe(200)
        return status.json()
      }, "Error")

      expect(body).toMatchObject({
        state: "Error",
      })
      expect(body.message).toContain("Failed to initialize: test indexing initialization failed")
    } finally {
      init.mockRestore()
    }
  })

  test("reports routes as in progress while initialization is in flight", async () => {
    await using tmp = await tmpdir({ git: true, config: cfg })
    process.env["SAEEOL_CONFIG_DIR"] = tmp.path
    const gate = Promise.withResolvers<{ requiresRestart: boolean }>()
    const init = spyOn(CodeIndexManager.prototype, "initialize").mockImplementation(() => gate.promise)

    try {
      const app = Server.Default().app

      const config = await app.request("/config", {
        headers: {
          "x-saeeol-directory": tmp.path,
        },
      })
      expect(config.status).toBe(200)
      await called(init)

      const status = await app.request("/indexing/status", {
        headers: {
          "x-saeeol-directory": tmp.path,
        },
      })
      expect(status.status).toBe(200)

      const body = await status.json()
      expect(body).toMatchObject({
        state: "In Progress",
        message: "Indexing is initializing.",
      })
    } finally {
      gate.resolve({ requiresRestart: false })
      init.mockRestore()
    }
  })

  test("does not publish initialized status after in-flight startup is disposed", async () => {
    await using tmp = await tmpdir({ git: true, config: cfg })
    process.env["SAEEOL_CONFIG_DIR"] = tmp.path
    const gate = Promise.withResolvers<{ requiresRestart: boolean }>()
    const init = spyOn(CodeIndexManager.prototype, "initialize").mockImplementation(() => gate.promise)
    const events: SaeeolIndexing.Status[] = []
    const on = (data: {
      directory?: string
      payload?: { type?: string; properties?: { status?: SaeeolIndexing.Status } }
    }) => {
      if (data.directory !== tmp.path) return
      if (data.payload?.type !== SaeeolIndexing.Event.type) return
      if (data.payload.properties?.status) events.push(data.payload.properties.status)
    }
    GlobalBus.on("event", on)

    try {
      await Instance.provide({
        directory: tmp.path,
        init: await getBootstrapRunEffect(),
        fn: async () => {
          await called(init)
          expect((await SaeeolIndexing.current()).state).toBe("In Progress")
        },
      })

      await disposeAllInstances()
      gate.resolve({ requiresRestart: false })
      await new Promise((resolve) => setTimeout(resolve, 0))

      expect(events.some((status) => status.state === "Complete" || status.state === "Standby")).toBe(false)
    } finally {
      GlobalBus.off("event", on)
      gate.resolve({ requiresRestart: false })
      init.mockRestore()
    }
  })

  test("keeps degraded indexing queryable but unavailable", async () => {
    const init = spyOn(CodeIndexManager.prototype, "initialize").mockRejectedValue(error)

    await using tmp = await tmpdir({ git: true, config: cfg })
    process.env["SAEEOL_CONFIG_DIR"] = tmp.path

    try {
      await Instance.provide({
        directory: tmp.path,
        init: await getBootstrapRunEffect(),
        fn: async () => {
          const status = await wait(() => SaeeolIndexing.current(), "Error")

          expect(status.state).toBe("Error")
          expect(status.message).toContain("Failed to initialize: test indexing initialization failed")
          expect(await SaeeolIndexing.available()).toBe(false)
          expect(SaeeolIndexing.ready()).toBe(false)
          expect(await SaeeolIndexing.search("boot failure")).toEqual([])
        },
      })
    } finally {
      init.mockRestore()
    }
  })

  test("reports not ready while initialization is in flight", async () => {
    await using tmp = await tmpdir({ git: true, config: cfg })
    process.env["SAEEOL_CONFIG_DIR"] = tmp.path
    const gate = Promise.withResolvers<{ requiresRestart: boolean }>()
    const init = spyOn(CodeIndexManager.prototype, "initialize").mockImplementation(() => gate.promise)

    try {
      await Instance.provide({
        directory: tmp.path,
        init: await getBootstrapRunEffect(),
        fn: async () => {
          await called(init)

          expect(init).toHaveBeenCalled()
          expect(SaeeolIndexing.ready()).toBe(false)
          expect(await SaeeolIndexing.available()).toBe(false)
          expect(await SaeeolIndexing.search("boot failure")).toEqual([])
        },
      })
    } finally {
      gate.resolve({ requiresRestart: false })
      init.mockRestore()
    }
  })

  test("stays disabled when semantic indexing flag is off", async () => {
    await using tmp = await tmpdir({ git: true, config: off })
    process.env["SAEEOL_CONFIG_DIR"] = tmp.path
    const init = spyOn(CodeIndexManager.prototype, "initialize")

    await Instance.provide({
      directory: tmp.path,
      init: await getBootstrapRunEffect(),
      fn: async () => {
        const status = await SaeeolIndexing.current()

        expect(status).toMatchObject({
          state: "Disabled",
          message: "Semantic indexing is disabled. Enable it in the Experimental settings.",
        })
        expect(await SaeeolIndexing.available()).toBe(false)
        expect(SaeeolIndexing.ready()).toBe(false)
        expect(await SaeeolIndexing.search("flag off")).toEqual([])
        expect(init).not.toHaveBeenCalled()
      },
    })
  })

  test("enriches Saeeol provider config from env auth", async () => {
    global.fetch = (() =>
      Promise.resolve(
        new Response(
          JSON.stringify({
            defaultModel: "mistralai/mistral-embed-2312",
            models: [
              { id: "mistralai/mistral-embed-2312", name: "Mistral Embed 2312", dimension: 1024, scoreThreshold: 0.35 },
            ],
            aliases: {},
          }),
        ),
      )) as unknown as typeof global.fetch
    const init = spyOn(CodeIndexManager.prototype, "initialize").mockResolvedValue({ requiresRestart: false })
    const key = process.env.SAEEOL_API_KEY
    const org = process.env.SAEEOL_ORG_ID

    await using tmp = await tmpdir({ git: true, config: saeeol })
    process.env["SAEEOL_CONFIG_DIR"] = tmp.path
    process.env.SAEEOL_API_KEY = "saeeol-token"
    process.env.SAEEOL_ORG_ID = "org_123"

    try {
      await Instance.provide({
        directory: tmp.path,
        init: await getBootstrapRunEffect(),
        fn: async () => {
          await called(init)
          expect(init.mock.calls[0]?.[0]).toMatchObject({
            embedderProvider: "saeeol",
            saeeolApiKey: "saeeol-token",
            saeeolOrganizationId: "org_123",
            modelId: "mistralai/mistral-embed-2312",
            modelDimension: 1024,
            searchMinScore: 0.35,
          })
        },
      })
    } finally {
      if (key === undefined) delete process.env.SAEEOL_API_KEY
      else process.env.SAEEOL_API_KEY = key
      if (org === undefined) delete process.env.SAEEOL_ORG_ID
      else process.env.SAEEOL_ORG_ID = org
      init.mockRestore()
    }
  })

  test("does not default to Saeeol when an existing provider config is present", async () => {
    const init = spyOn(CodeIndexManager.prototype, "initialize").mockResolvedValue({ requiresRestart: false })
    const key = process.env.SAEEOL_API_KEY

    await using tmp = await tmpdir({ git: true, config: implicitOpenAi })
    process.env["SAEEOL_CONFIG_DIR"] = tmp.path
    process.env.SAEEOL_API_KEY = "saeeol-token"

    try {
      await Instance.provide({
        directory: tmp.path,
        init: await getBootstrapRunEffect(),
        fn: async () => {
          await called(init)
          expect(init.mock.calls[0]?.[0]).toMatchObject({
            embedderProvider: "openai",
            openAiKey: "openai-token",
          })
        },
      })
    } finally {
      if (key === undefined) delete process.env.SAEEOL_API_KEY
      else process.env.SAEEOL_API_KEY = key
      init.mockRestore()
    }
  })

  test("stays disabled when VS Code starts without a workspace folder", async () => {
    await using tmp = await tmpdir({ git: true, config: cfg })
    process.env["SAEEOL_CONFIG_DIR"] = tmp.path
    process.env["SAEEOL_DISABLE_CODEBASE_INDEXING"] = "vscode-no-workspace"
    const init = spyOn(CodeIndexManager.prototype, "initialize")

    try {
      await Instance.provide({
        directory: tmp.path,
        init: await getBootstrapRunEffect(),
        fn: async () => {
          const status = await SaeeolIndexing.current()

          expect(status).toMatchObject({
            state: "Disabled",
            message: "Codebase indexing is disabled because no workspace folder is open in VS Code.",
          })
          expect(await SaeeolIndexing.available()).toBe(false)
          expect(SaeeolIndexing.ready()).toBe(false)
          expect(await SaeeolIndexing.search("no workspace")).toEqual([])
          expect(init).not.toHaveBeenCalled()
        },
      })
    } finally {
      init.mockRestore()
    }
  })
})
