import { describe, test, expect, beforeEach, afterEach } from "bun:test"
import { createNonInteractiveEnvHook, NON_INTERACTIVE_ENV } from "./index"

describe("non-interactive-env hook", () => {
  const mockCtx = {} as Parameters<typeof createNonInteractiveEnvHook>[0]

  let originalPlatform: NodeJS.Platform
  let originalEnv: Record<string, string | undefined>

  beforeEach(() => {
    originalPlatform = process.platform
    originalEnv = {
      SHELL: process.env.SHELL,
      PSModulePath: process.env.PSModulePath,
      MSYSTEM: process.env.MSYSTEM,
      ComSpec: process.env.ComSpec,
      CI: process.env.CI,
      OPENCODE_NON_INTERACTIVE: process.env.OPENCODE_NON_INTERACTIVE,
    }
    // given clean Unix-like environment for all tests
    // This prevents CI environments (which may have PSModulePath set) from
    // triggering PowerShell detection in tests that expect Unix behavior
    delete process.env.PSModulePath
    process.env.SHELL = "/bin/bash"
    process.env.OPENCODE_NON_INTERACTIVE = "true"
  })

  afterEach(() => {
    Object.defineProperty(process, "platform", { value: originalPlatform })
    for (const [key, value] of Object.entries(originalEnv)) {
      if (value !== undefined) {
        process.env[key] = value
      } else {
        delete process.env[key]
      }
    }
  })

  describe("git command modification", () => {
    test("#given git command #when hook executes #then prepends export statement", async () => {
      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git commit -m 'test'" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      const cmd = output.args.command as string
      expect(cmd).toStartWith("export ")
      expect(cmd).toContain("GIT_EDITOR=:")
      expect(cmd).toContain("EDITOR=:")
      expect(cmd).toContain("PAGER=cat")
      expect(cmd).toContain("; git commit -m 'test'")
    })

    test("#given chained git commands #when hook executes #then export applies to all", async () => {
      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git add file && git rebase --continue" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      const cmd = output.args.command as string
      expect(cmd).toStartWith("export ")
      expect(cmd).toContain("; git add file && git rebase --continue")
    })

    test("#given non-git bash command #when hook executes #then command unchanged", async () => {
      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "ls -la" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      expect(output.args.command).toBe("ls -la")
    })

    test("#given non-bash tool #when hook executes #then command unchanged", async () => {
      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git status" },
      }

      await hook["tool.execute.before"](
        { tool: "Read", sessionID: "test", callID: "1" },
        output
      )

      expect(output.args.command).toBe("git status")
    })

    test("#given empty command #when hook executes #then no error", async () => {
      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: {},
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      expect(output.args.command).toBeUndefined()
    })

    test("#given git command already has prefix #when hook executes again #then does not duplicate prefix", async () => {
      const hook = createNonInteractiveEnvHook(mockCtx)
      
      // First call: transforms the command
      const output1: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git commit -m 'test'" },
      }
      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output1
      )
      
      const firstResult = output1.args.command as string
      expect(firstResult).toStartWith("export ")
      
      // Second call: takes the already-prefixed command
      const output2: { args: Record<string, unknown>; message?: string } = {
        args: { command: firstResult },
      }
      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "2" },
        output2
      )
      
      // Should be exactly the same (no double prefix)
      expect(output2.args.command).toBe(firstResult)
    })
  })

  describe("shell escaping", () => {
    test("#given git command #when building prefix #then VISUAL properly escaped", async () => {
      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git status" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      const cmd = output.args.command as string
      expect(cmd).toContain("VISUAL=''")
    })

    test("#given git command #when building prefix #then all NON_INTERACTIVE_ENV vars included", async () => {
      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git log" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      const cmd = output.args.command as string
      for (const key of Object.keys(NON_INTERACTIVE_ENV)) {
        expect(cmd).toContain(`${key}=`)
      }
    })
  })

  describe("banned command detection", () => {
    test("#given vim command #when hook executes #then warning message set", async () => {
      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "vim file.txt" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      expect(output.message).toContain("vim")
      expect(output.message).toContain("interactive")
    })

    test("#given safe command #when hook executes #then no warning", async () => {
      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "ls -la" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      expect(output.message).toBeUndefined()
    })
  })

  describe("platform-aware shell syntax", () => {

    test("#given macOS platform #when git command executes #then uses unix export syntax", async () => {
      delete process.env.PSModulePath
      process.env.SHELL = "/bin/zsh"
      Object.defineProperty(process, "platform", { value: "darwin" })

      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git status" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      const cmd = output.args.command as string
      expect(cmd).toStartWith("export ")
      expect(cmd).toContain(";")
      expect(cmd).not.toContain("$env:")
      expect(cmd).not.toContain("set ")
    })

    test("#given Linux platform #when git command executes #then uses unix export syntax", async () => {
      delete process.env.PSModulePath
      process.env.SHELL = "/bin/bash"
      Object.defineProperty(process, "platform", { value: "linux" })

      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git commit -m 'test'" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      const cmd = output.args.command as string
      expect(cmd).toStartWith("export ")
      expect(cmd).toContain("; git commit")
    })

    test("#given Windows cmd environment with PSModulePath #when bash tool git command executes #then uses cmd syntax", async () => {
      delete process.env.SHELL
      delete process.env.MSYSTEM
      process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
      Object.defineProperty(process, "platform", { value: "win32" })

      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git status" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      const cmd = output.args.command as string
      expect(cmd).toStartWith("set ")
      expect(cmd).toContain(" && git status")
      expect(cmd).toContain('GIT_EDITOR=":"')
      expect(cmd).not.toContain("$env:")
      expect(cmd).not.toContain("export ")
    })

    test("#given Windows SHELL=cmd.exe #when bash tool git command executes #then uses cmd syntax", async () => {
      process.env.SHELL = "C:\\Windows\\System32\\cmd.exe"
      delete process.env.MSYSTEM
      process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
      Object.defineProperty(process, "platform", { value: "win32" })

      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git status" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      const cmd = output.args.command as string
      expect(cmd).toStartWith("set ")
      expect(cmd).toContain(" && git status")
      expect(cmd).not.toContain("$env:")
      expect(cmd).not.toContain("export ")
    })

    test("#given Windows ComSpec=pwsh.exe without SHELL #when bash tool git command executes #then uses powershell syntax", async () => {
      delete process.env.SHELL
      delete process.env.MSYSTEM
      process.env.ComSpec = "C:\\Program Files\\PowerShell\\7\\pwsh.exe"
      process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
      Object.defineProperty(process, "platform", { value: "win32" })

      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git status" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      const cmd = output.args.command as string
      expect(cmd).toStartWith("$env:")
      expect(cmd).toContain("; git status")
      expect(cmd).not.toContain("set ")
      expect(cmd).not.toContain("export ")
    })

    test("#given Windows SHELL=pwsh.exe #when bash tool git command executes #then uses powershell syntax", async () => {
      process.env.SHELL = "C:\\Program Files\\PowerShell\\7\\pwsh.exe"
      delete process.env.MSYSTEM
      process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
      Object.defineProperty(process, "platform", { value: "win32" })

      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git status" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      const cmd = output.args.command as string
      expect(cmd).toStartWith("$env:")
      expect(cmd).toContain("; git status")
      expect(cmd).not.toContain("set ")
      expect(cmd).not.toContain("export ")
    })

    test("#given Windows without SHELL env #when bash tool git command executes #then uses cmd syntax", async () => {
      delete process.env.PSModulePath
      delete process.env.SHELL
      delete process.env.MSYSTEM
      Object.defineProperty(process, "platform", { value: "win32" })

      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git log" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      const cmd = output.args.command as string
      expect(cmd).toStartWith("set ")
      expect(cmd).toContain(" && git log")
      expect(cmd).toContain('GIT_EDITOR=":"')
      expect(cmd).not.toContain("$env:")
      expect(cmd).not.toContain("export ")
    })

    test("#given Windows Git Bash environment with SHELL #when git command executes #then uses unix syntax", async () => {
      delete process.env.PSModulePath
      process.env.SHELL = "/usr/bin/bash"
      Object.defineProperty(process, "platform", { value: "win32" })

      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git status" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      const cmd = output.args.command as string
      expect(cmd).toStartWith("export ")
      expect(cmd).toContain("; git status")
      expect(cmd).not.toContain("$env:")
    })

    test("#given Windows Git Bash via MSYSTEM without SHELL #when git command executes #then uses unix syntax", async () => {
      delete process.env.SHELL
      process.env.MSYSTEM = "MINGW64"
      process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
      Object.defineProperty(process, "platform", { value: "win32" })

      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git status" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      const cmd = output.args.command as string
      expect(cmd).toStartWith("export ")
      expect(cmd).toContain("; git status")
      expect(cmd).not.toContain("$env:")
    })

    test("#given Windows platform #when chained git commands via bash tool #then uses cmd syntax", async () => {
      delete process.env.PSModulePath
      delete process.env.SHELL
      delete process.env.MSYSTEM
      Object.defineProperty(process, "platform", { value: "win32" })

      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git add file && git commit -m 'test'" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      const cmd = output.args.command as string
      expect(cmd).toStartWith("set ")
      expect(cmd).toContain(" && git add file && git commit")
      expect(cmd).toContain('GIT_EDITOR=":"')
      expect(cmd).not.toContain("export ")
      expect(cmd).not.toContain("$env:")
    })

    test("#given SHELL=/bin/bash on win32 #when git command executes #then uses unix syntax", async () => {
      // Git Bash or WSL sets SHELL env var - should override platform detection
      delete process.env.PSModulePath
      process.env.SHELL = "/bin/bash"
      Object.defineProperty(process, "platform", { value: "win32" })

      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git status" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      const cmd = output.args.command as string
      expect(cmd).toStartWith("export ")
      expect(cmd).toContain("; git status")
      expect(cmd).not.toContain("$env:")
    })

    test("#given PSModulePath set on non-Windows #when git command executes #then uses powershell syntax", async () => {
      // PowerShell detection via PSModulePath should work regardless of platform
      delete process.env.SHELL
      delete process.env.MSYSTEM
      process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
      Object.defineProperty(process, "platform", { value: "linux" })

      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git log" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      const cmd = output.args.command as string
      expect(cmd).toStartWith("$env:")
      expect(cmd).toContain("; git log")
      expect(cmd).not.toContain("export ")
    })

    test("#given no SHELL and no PSModulePath on win32 #when git command executes #then uses cmd syntax", async () => {
      // Platform fallback: win32 without env hints should use cmd
      delete process.env.SHELL
      delete process.env.PSModulePath
      delete process.env.MSYSTEM
      Object.defineProperty(process, "platform", { value: "win32" })

      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git status" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      const cmd = output.args.command as string
      expect(cmd).toStartWith("set ")
      expect(cmd).toContain(" && git status")
      expect(cmd).toContain('GIT_EDITOR=":"')
      expect(cmd).not.toContain("export ")
      expect(cmd).not.toContain("$env:")
    })

    test("#given no SHELL and no PSModulePath on linux #when git command executes #then uses unix syntax", async () => {
      // Platform fallback: non-win32 without env hints should use unix
      delete process.env.SHELL
      delete process.env.PSModulePath
      Object.defineProperty(process, "platform", { value: "linux" })

      const hook = createNonInteractiveEnvHook(mockCtx)
      const output: { args: Record<string, unknown>; message?: string } = {
        args: { command: "git status" },
      }

      await hook["tool.execute.before"](
        { tool: "bash", sessionID: "test", callID: "1" },
        output
      )

      const cmd = output.args.command as string
      expect(cmd).toStartWith("export ")
      expect(cmd).toContain("; git status")
      expect(cmd).not.toContain("$env:")
      expect(cmd).not.toContain("set ")
    })
  })
})
