import { describe, expect } from "bun:test"
import { Context, Effect, Exit, Fiber, Schema, Stream } from "effect"
import { Plugin as EffectPlugin } from "@opencode-ai/plugin/effect"
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
import { Agent } from "@opencode-ai/core/agent"
import { Bus } from "@opencode-ai/core/bus"
import { Plugin } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { Session } from "@opencode-ai/core/session"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { Tool } from "@opencode-ai/core/tool"
import { testEffect } from "./lib/effect"
import { PluginTestLayer } from "./plugin/fixture"

const it = testEffect(PluginTestLayer)

class Secret extends Context.Service<Secret, string>()("@opencode/test/PluginSecret") {}

const versioned = <R>(plugin: EffectPlugin.Plugin<R>, version = "1") => ({ ...plugin, version })

describe("Plugin", () => {
  it.live("exposes public events through the plugin context", () =>
    Effect.gen(function* () {
      const plugins = yield* Plugin.Service
      const bus = yield* Bus.Service
      const host = yield* PluginHost.make(plugins)
      const received = yield* host.event.subscribe().pipe(
        Stream.filter((event) => event.type === "config.updated"),
        Stream.runHead,
        Effect.forkScoped({ startImmediately: true }),
      )
      yield* Effect.sleep("10 millis")

      yield* bus.publish(ConfigSchema.Event.Updated, {})

      expect((yield* Fiber.join(received)).valueOrUndefined?.type).toBe("config.updated")
    }),
  )

  it.effect("replaces plugins by ID and version", () =>
    Effect.gen(function* () {
      const plugins = yield* Plugin.Service
      const agents = yield* Agent.Service
      const bus = yield* Bus.Service
      let description = "first"
      let updates = 0
      const unsubscribe = yield* bus.listen((event) =>
        Effect.sync(() => {
          if (event.type === Plugin.Event.Updated.type) updates++
        }),
      )

      const managed = () =>
        EffectPlugin.define({
          id: "managed",
          effect: (ctx) =>
            ctx.agent
              .transform((agents) =>
                agents.update("configured", (agent) => {
                  agent.description = description
                }),
              )
              .pipe(Effect.asVoid),
        })

      yield* plugins.activate([versioned(managed(), "1")])

      expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("first")

      description = "second"
      yield* plugins.activate([versioned(managed(), "2")])
      expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("second")

      description = "third"
      yield* plugins.activate([versioned(managed(), "2")])
      expect(updates).toBe(2)
      expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("second")

      yield* plugins.activate([])
      expect(yield* agents.get(Agent.ID.make("configured"))).toBeUndefined()
      expect(updates).toBe(3)
      yield* unsubscribe
    }),
  )

  it.effect("rejects duplicate IDs before replacing active plugins", () =>
    Effect.gen(function* () {
      const plugins = yield* Plugin.Service
      const active = Plugin.ID.make("active")
      const duplicate = "duplicate"
      yield* plugins.activate([{ id: active, version: "1", effect: () => Effect.void }])

      const result = yield* plugins
        .activate([
          { id: duplicate, version: "1", effect: () => Effect.void },
          { id: duplicate, version: "1", effect: () => Effect.void },
        ])
        .pipe(Effect.exit)

      expect(Exit.isFailure(result)).toBe(true)
      expect(yield* plugins.list()).toEqual([{ id: active }])
    }),
  )

  it.effect("skips failed plugins and loads the rest", () =>
    Effect.gen(function* () {
      const plugins = yield* Plugin.Service
      const agents = yield* Agent.Service
      let fail = true
      const good = EffectPlugin.define({
        id: "good",
        effect: (ctx) =>
          ctx.agent
            .transform((agents) =>
              agents.update("configured", (agent) => {
                agent.description = "loaded"
              }),
            )
            .pipe(Effect.asVoid),
      })
      const bad = EffectPlugin.define({
        id: "bad",
        effect: () => {
          if (fail) return Effect.die(new Error("materialization failed"))
          return Effect.void
        },
      })

      yield* plugins.activate([versioned(good), versioned(bad)])
      expect(yield* plugins.list()).toEqual([{ id: Plugin.ID.make("good") }])
      expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("loaded")

      fail = false
      yield* plugins.activate([versioned(good), versioned(bad, "2")])
      expect(yield* plugins.list()).toEqual([{ id: Plugin.ID.make("good") }, { id: Plugin.ID.make("bad") }])
    }),
  )

  it.effect("restores the previous plugin when its replacement fails", () =>
    Effect.gen(function* () {
      const plugins = yield* Plugin.Service
      const agents = yield* Agent.Service
      const previous = EffectPlugin.define({
        id: "managed",
        effect: (ctx) =>
          ctx.agent
            .transform((agents) =>
              agents.update("configured", (agent) => {
                agent.description = "previous"
              }),
            )
            .pipe(Effect.asVoid),
      })
      const replacement = EffectPlugin.define({
        id: "managed",
        effect: (ctx) =>
          Effect.gen(function* () {
            yield* ctx.agent.transform((agents) =>
              agents.update("configured", (agent) => {
                agent.description = "replacement"
              }),
            )
            return yield* Effect.die(new Error("replacement failed"))
          }),
      })

      yield* plugins.activate([versioned(previous)])
      yield* plugins.activate([versioned(replacement, "2")])

      expect(yield* plugins.list()).toEqual([{ id: Plugin.ID.make("managed") }])
      expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("previous")
    }),
  )

  it.effect("deactivates a plugin when replacement and restoration fail", () =>
    Effect.gen(function* () {
      const plugins = yield* Plugin.Service
      const agents = yield* Agent.Service
      let loads = 0
      const previous = EffectPlugin.define({
        id: "managed",
        effect: (ctx) => {
          loads++
          if (loads > 1) return Effect.die(new Error("restoration failed"))
          return ctx.agent
            .transform((agents) =>
              agents.update("configured", (agent) => {
                agent.description = "previous"
              }),
            )
            .pipe(Effect.asVoid)
        },
      })
      const replacement = EffectPlugin.define({
        id: "managed",
        effect: () => Effect.die(new Error("replacement failed")),
      })

      yield* plugins.activate([versioned(previous)])
      yield* plugins.activate([versioned(replacement, "2")])

      expect(yield* plugins.list()).toEqual([])
      expect(yield* agents.get(Agent.ID.make("configured"))).toBeUndefined()
    }),
  )

  it.effect("closes the previous generation in reverse order", () =>
    Effect.gen(function* () {
      const plugins = yield* Plugin.Service
      const closed: string[] = []
      yield* plugins.activate(
        ["first", "second"].map((id) => ({
          id,
          version: "1",
          effect: () => Effect.addFinalizer(() => Effect.sync(() => closed.push(id))),
        })),
      )

      yield* plugins.activate([])

      expect(closed).toEqual(["second", "first"])
    }),
  )

  it.effect("isolates plugins from ambient services", () =>
    Effect.gen(function* () {
      const plugins = yield* Plugin.Service
      let visible = true
      const plugin = EffectPlugin.define({
        id: "isolated",
        effect: () =>
          Effect.serviceOption(Secret).pipe(
            Effect.tap((secret) => Effect.sync(() => (visible = secret._tag === "Some"))),
            Effect.asVoid,
          ),
      })

      yield* plugins.activate([versioned(plugin)]).pipe(Effect.provideService(Secret, "secret"))

      expect(visible).toBe(false)
    }),
  )

  it.effect("registers location tools through the plugin context", () =>
    Effect.gen(function* () {
      const plugins = yield* Plugin.Service
      const registry = yield* Tool.Service
      const plugin = EffectPlugin.define({
        id: "tool-plugin",
        effect: (ctx) =>
          ctx.tool
            .transform((draft) =>
              draft.add(
                ({
                  name: "plugin_tool",
                  options: { codemode: false },
                  description: "Plugin tool",
                  input: Schema.Struct({}),
                  output: Schema.Struct({ ok: Schema.Boolean }),
                  execute: () => Effect.succeed({ output: { ok: true } }),
                }),
              ),
            )
            .pipe(Effect.orDie),
      })

      yield* plugins.activate([versioned(plugin)])
      expect((yield* registry.snapshot()).definitions.map((tool) => tool.name)).toContain("plugin_tool")

      yield* plugins.activate([])
      expect((yield* registry.snapshot()).definitions.map((tool) => tool.name)).not.toContain("plugin_tool")
    }),
  )

  it.effect("namespaces tool names and routes codemode registrations through execute", () =>
    Effect.gen(function* () {
      const plugins = yield* Plugin.Service
      const registry = yield* Tool.Service
      const tool = (name: string, description: string, options?: Tool.Options) =>
        ({
          name,
          options,
          description,
          input: Schema.Struct({}),
          output: Schema.Struct({ ok: Schema.Boolean }),
          execute: () => Effect.succeed({ output: { ok: true } }),
        })
      const plugin = EffectPlugin.define({
        id: "grouped-tools",
        effect: (ctx) =>
          ctx.tool
            .transform((draft) => {
              draft.add(tool("plain", "Plain", { codemode: false }))
              draft.add(tool("look/up", "Lookup", { namespace: "context7", codemode: false }))
              draft.add(tool("search", "Search", { namespace: "context7" }))
            })
            .pipe(Effect.orDie),
      })

      yield* plugins.activate([versioned(plugin)])

      expect((yield* registry.snapshot()).definitions.map((tool) => tool.name)).toEqual([
        "context7_look_up",
        "plain",
        "execute",
      ])
    }),
  )

  it.effect("fires before/after tool hooks with mutable events around execution", () =>
    Effect.gen(function* () {
      const plugins = yield* Plugin.Service
      const registry = yield* Tool.Service
      const executed: unknown[] = []
      const seen: {
        before?: unknown
        after?: { input: unknown; status: string; content: unknown; metadata: unknown }
      } = {}

      const plugin = EffectPlugin.define({
        id: "tool-hooks",
        effect: (ctx) =>
          Effect.gen(function* () {
            yield* ctx.tool
              .transform((draft) =>
                draft.add(
                  ({
                    name: "echo",
                    options: { codemode: false },
                    description: "Echo",
                    input: Schema.Struct({ text: Schema.String }),
                    output: Schema.Struct({ text: Schema.String }),
                    execute: ({ text }) =>
                      Effect.sync(() => executed.push({ text })).pipe(Effect.as({ output: { text } })),
                  }),
                ),
              )
              .pipe(Effect.orDie)

            yield* ctx.tool
              .hook("execute.before", (event) =>
                Effect.sync(() => {
                  seen.before = event.input
                  event.input = { text: "before-mutated" }
                }),
              )
              .pipe(Effect.asVoid)

            yield* ctx.tool
              .hook("execute.after", (event) =>
                Effect.sync(() => {
                  seen.after = {
                    input: event.input,
                    status: event.status,
                    content: event.status === "completed" ? event.result.content : undefined,
                    metadata: event.status === "completed" ? event.result.metadata : event.error.metadata,
                  }
                  if (event.status !== "completed") return
                  event.result = {
                    ...event.result,
                    content: [{ type: "text", text: "after-mutated" }],
                    metadata: { rewritten: true },
                  }
                }),
              )
              .pipe(Effect.asVoid)

            yield* ctx.tool
              .hook("execute.after", (event) =>
                Effect.sync(() => {
                  if (event.status === "completed" && Array.isArray(event.result.content)) event.result.content.splice(0)
                }),
              )
              .pipe(Effect.asVoid)
          }),
      })

      yield* plugins.activate([versioned(plugin)])

      const toolSet = yield* registry.snapshot()
      const execution = yield* toolSet.execute({
        sessionID: Session.ID.make("ses_hooks"),
        agent: Agent.ID.make("build"),
        messageID: SessionMessage.ID.make("msg_hooks"),
        call: { type: "tool-call", id: "call-hooks", name: "echo", input: { text: "original" } },
      })

      expect(seen.before).toEqual({ text: "original" })
      expect(executed).toEqual([{ text: "before-mutated" }])
      expect(seen.after).toEqual({
        input: { text: "before-mutated" },
        status: "completed",
        content: [{ type: "text", text: '{"text":"before-mutated"}' }],
        metadata: undefined,
      })
      expect(execution).toMatchObject({
        content: [{ type: "text", text: '{"text":"before-mutated"}' }],
        metadata: { rewritten: true },
      })
    }),
  )
})
