import { describe, it, expect, spyOn, beforeEach, afterEach } from "bun:test"
import * as opencode from "./opencode"
import { MIN_OPENCODE_VERSION } from "../constants"

describe("opencode check", () => {
  describe("compareVersions", () => {
    it("returns true when current >= minimum", () => {
      // given versions where current is greater
      // when comparing
      // then should return true
      expect(opencode.compareVersions("1.0.200", "1.0.150")).toBe(true)
      expect(opencode.compareVersions("1.1.0", "1.0.150")).toBe(true)
      expect(opencode.compareVersions("2.0.0", "1.0.150")).toBe(true)
    })

    it("returns true when versions are equal", () => {
      // given equal versions
      // when comparing
      // then should return true
      expect(opencode.compareVersions("1.0.150", "1.0.150")).toBe(true)
    })

    it("returns false when current < minimum", () => {
      // given version below minimum
      // when comparing
      // then should return false
      expect(opencode.compareVersions("1.0.100", "1.0.150")).toBe(false)
      expect(opencode.compareVersions("0.9.0", "1.0.150")).toBe(false)
    })

    it("handles version prefixes", () => {
      // given version with v prefix
      // when comparing
      // then should strip prefix and compare correctly
      expect(opencode.compareVersions("v1.0.200", "1.0.150")).toBe(true)
    })

    it("handles prerelease versions", () => {
      // given prerelease version
      // when comparing
      // then should use base version
      expect(opencode.compareVersions("1.0.200-beta.1", "1.0.150")).toBe(true)
    })
  })

  describe("command helpers", () => {
    it("selects where on Windows", () => {
      // given win32 platform
      // when selecting lookup command
      // then should use where
      expect(opencode.getBinaryLookupCommand("win32")).toBe("where")
    })

    it("selects which on non-Windows", () => {
      // given linux platform
      // when selecting lookup command
      // then should use which
      expect(opencode.getBinaryLookupCommand("linux")).toBe("which")
      expect(opencode.getBinaryLookupCommand("darwin")).toBe("which")
    })

    it("parses command output into paths", () => {
      // given raw output with multiple lines and spaces
      const output = "C:\\\\bin\\\\opencode.ps1\r\nC:\\\\bin\\\\opencode.exe\n\n"

      // when parsing
      const paths = opencode.parseBinaryPaths(output)

      // then should return trimmed, non-empty paths
      expect(paths).toEqual(["C:\\\\bin\\\\opencode.ps1", "C:\\\\bin\\\\opencode.exe"])
    })

    it("prefers exe/cmd/bat over ps1 on Windows", () => {
      // given windows paths
      const paths = [
        "C:\\\\bin\\\\opencode.ps1",
        "C:\\\\bin\\\\opencode.cmd",
        "C:\\\\bin\\\\opencode.exe",
      ]

      // when selecting binary
      const selected = opencode.selectBinaryPath(paths, "win32")

      // then should prefer exe
      expect(selected).toBe("C:\\\\bin\\\\opencode.exe")
    })

    it("falls back to ps1 when it is the only Windows candidate", () => {
      // given only ps1 path
      const paths = ["C:\\\\bin\\\\opencode.ps1"]

      // when selecting binary
      const selected = opencode.selectBinaryPath(paths, "win32")

      // then should return ps1 path
      expect(selected).toBe("C:\\\\bin\\\\opencode.ps1")
    })

    it("builds PowerShell command for ps1 on Windows", () => {
      // given a ps1 path on Windows
      const command = opencode.buildVersionCommand(
        "C:\\\\bin\\\\opencode.ps1",
        "win32"
      )

      // when building command
      // then should use PowerShell
      expect(command).toEqual([
        "powershell",
        "-NoProfile",
        "-ExecutionPolicy",
        "Bypass",
        "-File",
        "C:\\\\bin\\\\opencode.ps1",
        "--version",
      ])
    })

    it("builds direct command for non-ps1 binaries", () => {
      // given an exe on Windows and a binary on linux
      const winCommand = opencode.buildVersionCommand(
        "C:\\\\bin\\\\opencode.exe",
        "win32"
      )
      const linuxCommand = opencode.buildVersionCommand("opencode", "linux")

      // when building commands
      // then should execute directly
      expect(winCommand).toEqual(["C:\\\\bin\\\\opencode.exe", "--version"])
      expect(linuxCommand).toEqual(["opencode", "--version"])
    })
  })

  describe("getOpenCodeInfo", () => {
    it("returns installed: false when binary not found", async () => {
      // given no opencode binary
      const spy = spyOn(opencode, "findOpenCodeBinary").mockResolvedValue(null)

      // when getting info
      const info = await opencode.getOpenCodeInfo()

      // then should indicate not installed
      expect(info.installed).toBe(false)
      expect(info.version).toBeNull()
      expect(info.path).toBeNull()
      expect(info.binary).toBeNull()

      spy.mockRestore()
    })
  })

  describe("checkOpenCodeInstallation", () => {
    let getInfoSpy: ReturnType<typeof spyOn>

    afterEach(() => {
      getInfoSpy?.mockRestore()
    })

    it("returns fail when not installed", async () => {
      // given opencode not installed
      getInfoSpy = spyOn(opencode, "getOpenCodeInfo").mockResolvedValue({
        installed: false,
        version: null,
        path: null,
        binary: null,
      })

      // when checking installation
      const result = await opencode.checkOpenCodeInstallation()

      // then should fail with installation hint
      expect(result.status).toBe("fail")
      expect(result.message).toContain("not installed")
      expect(result.details).toBeDefined()
      expect(result.details?.some((d) => d.includes("opencode.ai"))).toBe(true)
    })

    it("returns warn when version below minimum", async () => {
      // given old version installed
      getInfoSpy = spyOn(opencode, "getOpenCodeInfo").mockResolvedValue({
        installed: true,
        version: "1.0.100",
        path: "/usr/local/bin/opencode",
        binary: "opencode",
      })

      // when checking installation
      const result = await opencode.checkOpenCodeInstallation()

      // then should warn about old version
      expect(result.status).toBe("warn")
      expect(result.message).toContain("below minimum")
      expect(result.details?.some((d) => d.includes(MIN_OPENCODE_VERSION))).toBe(true)
    })

    it("returns pass when properly installed", async () => {
      // given current version installed
      getInfoSpy = spyOn(opencode, "getOpenCodeInfo").mockResolvedValue({
        installed: true,
        version: "1.0.200",
        path: "/usr/local/bin/opencode",
        binary: "opencode",
      })

      // when checking installation
      const result = await opencode.checkOpenCodeInstallation()

      // then should pass
      expect(result.status).toBe("pass")
      expect(result.message).toContain("1.0.200")
    })
  })

  describe("getOpenCodeCheckDefinition", () => {
    it("returns valid check definition", () => {
      // given
      // when getting definition
      const def = opencode.getOpenCodeCheckDefinition()

      // then should have required properties
      expect(def.id).toBe("opencode-installation")
      expect(def.category).toBe("installation")
      expect(def.critical).toBe(true)
      expect(typeof def.check).toBe("function")
    })
  })

  describe("getDesktopAppPaths", () => {
    it("returns macOS desktop app paths for darwin platform", () => {
      // given darwin platform
      const platform: NodeJS.Platform = "darwin"

      // when getting desktop paths
      const paths = opencode.getDesktopAppPaths(platform)

      // then should include macOS app bundle paths with correct binary name
      expect(paths).toContain("/Applications/OpenCode.app/Contents/MacOS/OpenCode")
      expect(paths.some((p) => p.includes("Applications/OpenCode.app"))).toBe(true)
    })

    it("returns Windows desktop app paths for win32 platform when env vars set", () => {
      // given win32 platform with env vars set
      const platform: NodeJS.Platform = "win32"
      const originalProgramFiles = process.env.ProgramFiles
      const originalLocalAppData = process.env.LOCALAPPDATA
      process.env.ProgramFiles = "C:\\Program Files"
      process.env.LOCALAPPDATA = "C:\\Users\\Test\\AppData\\Local"

      // when getting desktop paths
      const paths = opencode.getDesktopAppPaths(platform)

      // then should include Windows program paths with correct binary name
      expect(paths.some((p) => p.includes("Program Files"))).toBe(true)
      expect(paths.some((p) => p.endsWith("OpenCode.exe"))).toBe(true)
      expect(paths.every((p) => p.startsWith("C:\\"))).toBe(true)

      // cleanup
      process.env.ProgramFiles = originalProgramFiles
      process.env.LOCALAPPDATA = originalLocalAppData
    })

    it("returns empty array for win32 when all env vars undefined", () => {
      // given win32 platform with no env vars
      const platform: NodeJS.Platform = "win32"
      const originalProgramFiles = process.env.ProgramFiles
      const originalLocalAppData = process.env.LOCALAPPDATA
      delete process.env.ProgramFiles
      delete process.env.LOCALAPPDATA

      // when getting desktop paths
      const paths = opencode.getDesktopAppPaths(platform)

      // then should return empty array (no relative paths)
      expect(paths).toEqual([])

      // cleanup
      process.env.ProgramFiles = originalProgramFiles
      process.env.LOCALAPPDATA = originalLocalAppData
    })

    it("returns Linux desktop app paths for linux platform", () => {
      // given linux platform
      const platform: NodeJS.Platform = "linux"

      // when getting desktop paths
      const paths = opencode.getDesktopAppPaths(platform)

      // then should include verified Linux installation paths
      expect(paths).toContain("/usr/bin/opencode")
      expect(paths).toContain("/usr/lib/opencode/opencode")
      expect(paths.some((p) => p.includes("AppImage"))).toBe(true)
    })

    it("returns empty array for unsupported platforms", () => {
      // given unsupported platform
      const platform = "freebsd" as NodeJS.Platform

      // when getting desktop paths
      const paths = opencode.getDesktopAppPaths(platform)

      // then should return empty array
      expect(paths).toEqual([])
    })
  })

  describe("findOpenCodeBinary with desktop fallback", () => {
    it("falls back to desktop paths when PATH binary not found", async () => {
      // given no binary in PATH but desktop app exists
      const existsSyncMock = (p: string) =>
        p === "/Applications/OpenCode.app/Contents/MacOS/OpenCode"

      // when finding binary with mocked filesystem
      const result = await opencode.findDesktopBinary("darwin", existsSyncMock)

      // then should find desktop app
      expect(result).not.toBeNull()
      expect(result?.path).toBe("/Applications/OpenCode.app/Contents/MacOS/OpenCode")
    })

    it("returns null when no desktop binary found", async () => {
      // given no binary exists
      const existsSyncMock = () => false

      // when finding binary
      const result = await opencode.findDesktopBinary("darwin", existsSyncMock)

      // then should return null
      expect(result).toBeNull()
    })
  })
})
