import { describe, expect, test } from "bun:test"
import { Context, Effect, Layer, LayerMap, Option } from "effect"
import { Node } from "@opencode-ai/util/effect/app-node"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { Location } from "@opencode-ai/core/location"
import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
import type { LocationError, LocationServices } from "@opencode-ai/core/location-services"
import { Project } from "@opencode-ai/core/project"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { tmpdir } from "../../fixture/tmpdir"

class Value extends Context.Service<Value, { readonly value: string }>()("test/TagValue") {}
class Result extends Context.Service<Result, { readonly value: string }>()("test/TagResult") {}
class CycleA extends Context.Service<CycleA, {}>()("test/NodeBuildA") {}
class CycleB extends Context.Service<CycleB, { readonly directory: AbsolutePath }>()("test/NodeBuildB") {}

describe("node build", () => {
  test("does not build a location service map when the graph does not require it", async () => {
    const result = Node.makeGlobalNode({
      service: Result,
      layer: Layer.succeed(Result, Result.of({ value: "plain" })),
      deps: [],
    })
    const layer = AppNodeBuilder.build(result)
    const program = Effect.gen(function* () {
      expect(Option.isNone(yield* Effect.serviceOption(LocationServiceMap.Service))).toBe(true)
      return (yield* Result).value
    }).pipe(Effect.provide(layer))

    expect(await Effect.runPromise(program)).toBe("plain")
  })

  test("detects cycles through a replaced location service map", async () => {
    const a = Node.makeGlobalNode({
      service: CycleA,
      layer: Layer.effect(CycleA, Effect.as(LocationServiceMap.Service, CycleA.of({}))),
      deps: [LocationServiceMap.node],
    })
    const b = Node.makeGlobalNode({
      service: CycleB,
      layer: Layer.effect(
        CycleB,
        Effect.map(CycleA, () => CycleB.of({ directory: AbsolutePath.make(process.cwd()) })),
      ),
      deps: [a],
    })
    const mapLayer = Layer.effect(
      LocationServiceMap.Service,
      Effect.gen(function* () {
        const service = yield* CycleB
        return yield* LayerMap.make(
          (ref: Location.Ref) =>
            Layer.succeed(
              Location.Service,
              Location.Service.of({
                directory: ref.directory,
                workspaceID: ref.workspaceID,
                project: { id: Project.ID.global, directory: service.directory, canonical: service.directory },
              }),
            ),
          { idleTimeToLive: "1 minute" },
        )
      }) as unknown as Effect.Effect<LayerMap.LayerMap<Location.Ref, LocationServices, LocationError>, never, CycleB>,
    )
    const map = Node.makeGlobalNode({ service: LocationServiceMap.Service, layer: mapLayer, deps: [b] })
    expect(() => AppNodeBuilder.build(LayerNode.group([a]), [[LocationServiceMap.node, map]])).toThrow(
      "Cycle detected in layer tree",
    )
  })

  test("shares top-level project with location services", async () => {
    await using tmp = await tmpdir()
    let acquisitions = 0
    const projectLayer = Layer.effect(
      Project.Service,
      Effect.sync(() => {
        acquisitions++
        return Project.Service.of({
          list: () => Effect.succeed([]),
          directories: () => Effect.succeed([]),
          resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory, canonical: directory }),
        })
      }),
    )
    const ref = Location.Ref.make({ directory: AbsolutePath.make(tmp.path) })
    const layer = AppNodeBuilder.build(LayerNode.group([Project.node, LocationServiceMap.node]), [
      [Project.node, projectLayer],
    ])
    const program = Effect.gen(function* () {
      yield* Project.Service
      const locations = yield* LocationServiceMap.Service
      expect(Option.isSome(yield* Effect.serviceOption(LocationServiceMap.Service))).toBe(true)
      return yield* Location.Service.pipe(Effect.provide(locations.get(ref)))
    }).pipe(Effect.provide(layer))

    expect((await Effect.runPromise(program)).directory).toBe(ref.directory)
    expect(acquisitions).toBe(1)
  })

  test("returns a composed application layer", async () => {
    const value = Node.makeGlobalNode({
      service: Value,
      layer: Layer.succeed(Value, Value.of({ value: "value" })),
      deps: [],
    })
    const result = Node.makeGlobalNode({
      service: Result,
      layer: Layer.effect(
        Result,
        Effect.gen(function* () {
          return Result.of({ value: (yield* Value).value })
        }),
      ),
      deps: [value],
    })
    const serviceLayer = AppNodeBuilder.build(result)
    const program = Effect.gen(function* () {
      expect(Option.isNone(yield* Effect.serviceOption(LocationServiceMap.Service))).toBe(true)
      return (yield* Result).value
    }).pipe(Effect.provide(serviceLayer))

    expect(await Effect.runPromise(program)).toBe("value")
  })
})
