import { describe, expect } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { Effect, Schema } from "effect"
import { AgentV2 } from "@novaclaw/core/agent"
import { AgentConfigStore } from "@novaclaw/core/agent-config-store"
import type { ConfigAgent } from "@novaclaw/core/config/agent"
import { Config } from "@novaclaw/core/config"
import { ConfigAgentPlugin } from "@novaclaw/core/config/plugin/agent"
import { AppNodeBuilder } from "@novaclaw/core/effect/app-node-builder"
import { LayerNode } from "@novaclaw/core/effect/layer-node"
import { FSUtil } from "@novaclaw/core/fs-util"
import { PermissionV2 } from "@novaclaw/core/permission"
import { AbsolutePath } from "@novaclaw/core/schema"
import { tmpdir } from "../fixture/tmpdir"
import { testEffect } from "../lib/effect"
import { agentHost, host } from "../plugin/host"

const it = testEffect(AppNodeBuilder.build(LayerNode.group([AgentV2.node, FSUtil.node])))

// Config→SQLite steps 2 + 8c: the plugin reads config-borne agents from the instance-wide
// store (pre-populated here — the import seeds fill it at boot; documents only carry the
// global `permissions` ruleset, which in production is the settings store's synthetic doc).
const memoryStore = () => {
  const layers = new Map<string, ConfigAgent.Info[]>()
  let defaultAgent: string | undefined
  return AgentConfigStore.Service.of({
    agents: () => Effect.sync(() => Object.fromEntries(layers)),
    setLayers: (name, next) =>
      Effect.sync(() => {
        layers.set(name, [...next])
      }),
    removeAgent: (name) =>
      Effect.sync(() => {
        layers.delete(name)
      }),
    getDefault: () => Effect.sync(() => defaultAgent),
    setDefault: (name) =>
      Effect.sync(() => {
        defaultAgent = name
      }),
    setDefaultIfEmpty: (name) =>
      Effect.sync(() => {
        defaultAgent ??= name
      }),
    isEmpty: () => Effect.sync(() => layers.size === 0),
  })
}
const decode = Schema.decodeUnknownSync(Config.Info)

describe("ConfigAgentPlugin.Plugin", () => {
  it.effect("applies all global permissions before agent-specific permissions", () =>
    Effect.gen(function* () {
      const agents = yield* AgentV2.Service
      const build = AgentV2.ID.make("build")
      yield* agents.transform((editor) =>
        editor.update(build, (agent) => {
          agent.mode = "primary"
          agent.permissions.push({ action: "bash", resource: "*", effect: "allow" })
        }),
      )

      const first = decode({
        agents: {
          build: {
            permissions: [{ action: "bash", resource: "git *", effect: "allow" }],
          },
          reviewer: {
            model: "openrouter/openai/gpt-5",
            description: "Review changes",
            mode: "subagent",
            permissions: [
              { action: "edit", resource: "*", effect: "deny" },
              { action: "read", resource: "*", effect: "deny" },
            ],
          },
          removed: { description: "Removed later" },
        },
      }).agents!
      const second = decode({
        agents: {
          reviewer: { variant: "high", hidden: true },
          removed: { disabled: true },
          late: {
            permissions: [{ action: "edit", resource: "*", effect: "allow" }],
          },
        },
      }).agents!
      const store = memoryStore()
      yield* store.setLayers("build", [first.build])
      yield* store.setLayers("reviewer", [first.reviewer, second.reviewer])
      yield* store.setLayers("removed", [first.removed, second.removed])
      yield* store.setLayers("late", [second.late])

      const config = Config.Service.of({
        entries: () =>
          Effect.succeed([
            new Config.Document({
              type: "document",
              info: decode({
                permissions: [
                  { action: "bash", resource: "*", effect: "ask" },
                  { action: "read", resource: "*", effect: "allow" },
                ],
              }),
            }),
          ]),
      })

      yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
        Effect.provideService(Config.Service, config),
        Effect.provideService(AgentConfigStore.Service, store),
      )

      const buildAgent = yield* agents.get(build)
      if (!buildAgent) throw new Error("expected configured build agent")
      expect(buildAgent.permissions).toEqual([
        { action: "bash", resource: "*", effect: "allow" },
        { action: "bash", resource: "*", effect: "ask" },
        { action: "read", resource: "*", effect: "allow" },
        { action: "bash", resource: "git *", effect: "allow" },
      ])
      expect(PermissionV2.evaluate("bash", "git status", buildAgent.permissions).effect).toBe("allow")
      expect(PermissionV2.evaluate("bash", "bun test", buildAgent.permissions).effect).toBe("ask")

      const reviewer = yield* agents.get(AgentV2.ID.make("reviewer"))
      if (!reviewer) throw new Error("expected configured reviewer agent")
      expect(reviewer).toMatchObject({
        description: "Review changes",
        mode: "subagent",
        hidden: true,
        model: { providerID: "openrouter", id: "openai/gpt-5", variant: "high" },
      })
      expect(reviewer.permissions).toEqual([
        { action: "bash", resource: "*", effect: "ask" },
        { action: "read", resource: "*", effect: "allow" },
        { action: "edit", resource: "*", effect: "deny" },
        { action: "read", resource: "*", effect: "deny" },
      ])
      expect(PermissionV2.evaluate("read", "README.md", reviewer.permissions).effect).toBe("deny")
      expect((yield* agents.get(AgentV2.ID.make("late")))?.permissions).toEqual([
        { action: "bash", resource: "*", effect: "ask" },
        { action: "read", resource: "*", effect: "allow" },
        { action: "edit", resource: "*", effect: "allow" },
      ])
      expect(yield* agents.get(AgentV2.ID.make("removed"))).toBeUndefined()
    }),
  )

  it.effect("maps configured agent fields and preserves an unspecified model variant", () =>
    Effect.gen(function* () {
      const agents = yield* AgentV2.Service
      const store = memoryStore()
      yield* store.setLayers("reviewer", [
        decode({
          agents: {
            reviewer: {
              model: "anthropic/claude-sonnet",
              system: "Review carefully.",
              description: "Reviews changes",
              mode: "subagent",
              hidden: true,
              color: "warning",
              steps: 12,
              request: {
                headers: { first: "one", shared: "first" },
                body: { enabled: true, profile: "review", effort: "medium" },
              },
            },
          },
        }).agents!.reviewer,
        decode({
          agents: {
            reviewer: {
              request: {
                headers: { shared: "last", second: "two" },
                body: { retries: 2, effort: "high" },
              },
            },
          },
        }).agents!.reviewer,
      ])

      yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
        Effect.provideService(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) })),
        Effect.provideService(AgentConfigStore.Service, store),
      )

      const reviewer = yield* agents.get(AgentV2.ID.make("reviewer"))
      if (!reviewer) throw new Error("expected configured reviewer agent")
      expect(reviewer).toMatchObject({
        system: "Review carefully.",
        description: "Reviews changes",
        mode: "subagent",
        hidden: true,
        color: "warning",
        steps: 12,
        model: { providerID: "anthropic", id: "claude-sonnet", variant: undefined },
      })
      expect(reviewer.request).toEqual({
        headers: { first: "one", shared: "last", second: "two" },
        body: { enabled: true, profile: "review", retries: 2, effort: "high" },
      })
    }),
  )

  it.effect("removes a built-in agent disabled by configuration", () =>
    Effect.gen(function* () {
      const agents = yield* AgentV2.Service
      const build = AgentV2.ID.make("build")
      yield* agents.transform((editor) => editor.update(build, () => {}))

      const store = memoryStore()
      yield* store.setLayers("build", [decode({ agents: { build: { disabled: true } } }).agents!.build])

      yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
        Effect.provideService(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) })),
        Effect.provideService(AgentConfigStore.Service, store),
      )

      expect(yield* agents.get(build)).toBeUndefined()
    }),
  )

  it.live("loads file-based agents from config directories", () =>
    Effect.acquireRelease(
      Effect.promise(() => tmpdir()),
      (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
    ).pipe(
      Effect.flatMap((tmp) =>
        Effect.gen(function* () {
          yield* Effect.promise(async () => {
            await fs.mkdir(path.join(tmp.path, "agents", "team"), { recursive: true })
            await fs.mkdir(path.join(tmp.path, "modes"), { recursive: true })
            await fs.writeFile(
              path.join(tmp.path, "agents", "reviewer.md"),
              `---
model: openrouter/openai/gpt-5
description: Markdown description
request:
  body:
    temperature: 0.5
permissions:
  - action: edit
    resource: "*"
    effect: deny
---
Review carefully.`,
            )
            await fs.writeFile(path.join(tmp.path, "agents", "team", "helper.md"), "Help the team.")
            await fs.writeFile(
              path.join(tmp.path, "agents", "native.md"),
              `---
request:
  headers:
    x-agent: native
  body:
    effort: high
permissions:
  - action: edit
    resource: "*"
    effect: deny
---
Use native v2 fields.`,
            )
            await fs.writeFile(path.join(tmp.path, "agents", "disabled.md"), "---\ndisabled: true\n---\nDisabled")
            await fs.writeFile(path.join(tmp.path, "modes", "plan.md"), "Make a plan.")
          })
          const agents = yield* AgentV2.Service
          const config = Config.Service.of({
            entries: () =>
              Effect.succeed([
                new Config.Document({
                  type: "document",
                  info: decode({ agents: { reviewer: { description: "JSON description" } } }),
                }),
                new Config.Directory({ type: "directory", path: AbsolutePath.make(tmp.path) }),
              ]),
          })

          yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
            Effect.provideService(Config.Service, config),
        Effect.provideService(AgentConfigStore.Service, memoryStore()),
          )

          expect(yield* agents.get(AgentV2.ID.make("reviewer"))).toMatchObject({
            model: { providerID: "openrouter", id: "openai/gpt-5" },
            system: "Review carefully.",
            description: "Markdown description",
            request: { body: { temperature: 0.5 } },
            permissions: [{ action: "edit", resource: "*", effect: "deny" }],
          })
          expect(yield* agents.get(AgentV2.ID.make("team/helper"))).toMatchObject({ system: "Help the team." })
          expect(yield* agents.get(AgentV2.ID.make("native"))).toMatchObject({
            system: "Use native v2 fields.",
            request: { headers: { "x-agent": "native" }, body: { effort: "high" } },
            permissions: [{ action: "edit", resource: "*", effect: "deny" }],
          })
          expect(yield* agents.get(AgentV2.ID.make("disabled"))).toBeUndefined()
          expect(yield* agents.get(AgentV2.ID.make("plan"))).toMatchObject({ system: "Make a plan.", mode: "primary" })
        }),
      ),
    ),
  )
})
