import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { delimiter, join } from "node:path";

const IS_WINDOWS = process.platform === "win32";
const HOME = IS_WINDOWS ? "C:\\Users\\testuser" : "/home/testuser";

// ─── Hoisted mocks ──────────────────────────────────────────────────────────

const mockExecSync = vi.hoisted(() => vi.fn());
const mockExistsSync = vi.hoisted(() => vi.fn((_path: string) => false));
const mockReaddirSync = vi.hoisted(() => vi.fn((_path: string) => [] as string[]));
const mockHomedir = vi.hoisted(() =>
  vi.fn(() => (process.platform === "win32" ? "C:\\Users\\testuser" : "/home/testuser")),
);

vi.mock("node:child_process", () => ({ execSync: mockExecSync }));

vi.mock("node:fs", async (importOriginal) => {
  const actual = await importOriginal<typeof import("node:fs")>();
  return {
    ...actual,
    existsSync: mockExistsSync,
    readdirSync: mockReaddirSync,
  };
});

vi.mock("node:os", async (importOriginal) => {
  const actual = await importOriginal<typeof import("node:os")>();
  return {
    ...actual,
    homedir: mockHomedir,
  };
});

// ─── Import after mocks ─────────────────────────────────────────────────────

import {
  captureUserShellPath,
  buildFallbackPath,
  getEnrichedPath,
  resolveBinary,
  getServicePath,
  _resetPathCache,
} from "./path-resolver.js";

// ─── Helpers ────────────────────────────────────────────────────────────────

const originalEnv = { ...process.env };

beforeEach(() => {
  vi.clearAllMocks();
  _resetPathCache();
  process.env = { ...originalEnv };
});

afterEach(() => {
  process.env = originalEnv;
});

// ─── captureUserShellPath ───────────────────────────────────────────────────

describe("captureUserShellPath", () => {
  it.skipIf(IS_WINDOWS)(
    "extracts PATH from login shell output using sentinel markers",
    () => {
      mockExecSync.mockReturnValueOnce(
        "___PATH_START___/usr/bin:/home/testuser/.nvm/versions/node/v20/bin:/home/testuser/.cargo/bin___PATH_END___\n",
      );

      const result = captureUserShellPath();
      expect(result).toBe(
        "/usr/bin:/home/testuser/.nvm/versions/node/v20/bin:/home/testuser/.cargo/bin",
      );
    },
  );

  it.skipIf(IS_WINDOWS)(
    "handles noisy shell output (MOTD, warnings) before and after PATH",
    () => {
      mockExecSync.mockReturnValueOnce(
        "Last login: Mon Jan 1\nWelcome!\n___PATH_START___/usr/local/bin:/usr/bin___PATH_END___\nbye\n",
      );

      const result = captureUserShellPath();
      expect(result).toBe("/usr/local/bin:/usr/bin");
    },
  );

  it.skipIf(IS_WINDOWS)(
    "falls back to buildFallbackPath when shell sourcing fails",
    () => {
      mockExecSync.mockImplementationOnce(() => {
        throw new Error("shell failed");
      });
      mockExistsSync.mockImplementation((p: string) =>
        p === "/usr/bin" || p === "/bin",
      );

      const result = captureUserShellPath();
      expect(result).toContain("/usr/bin");
      expect(result).toContain("/bin");
    },
  );

  it.skipIf(IS_WINDOWS)(
    "falls back when shell output contains no sentinel markers",
    () => {
      mockExecSync.mockReturnValueOnce("some garbage output\n");
      mockExistsSync.mockImplementation((p: string) => p === "/usr/bin");

      const result = captureUserShellPath();
      expect(result).toContain("/usr/bin");
    },
  );

  it.skipIf(IS_WINDOWS)(
    "uses $SHELL env var for the shell command",
    () => {
      process.env.SHELL = "/bin/zsh";
      mockExecSync.mockReturnValueOnce(
        "___PATH_START___/usr/bin___PATH_END___\n",
      );

      captureUserShellPath();

      expect(mockExecSync).toHaveBeenCalledWith(
        expect.stringContaining("/bin/zsh"),
        expect.any(Object),
      );
    },
  );

  it.skipIf(IS_WINDOWS)(
    "defaults to /bin/bash when $SHELL is not set",
    () => {
      delete process.env.SHELL;
      mockExecSync.mockReturnValueOnce(
        "___PATH_START___/usr/bin___PATH_END___\n",
      );

      captureUserShellPath();

      expect(mockExecSync).toHaveBeenCalledWith(
        expect.stringContaining("/bin/bash"),
        expect.any(Object),
      );
    },
  );

  it.skipIf(!IS_WINDOWS)(
    "on Windows, skips shell sourcing and uses fallback path directly",
    () => {
      const bunBin = join(HOME, ".bun", "bin");
      mockExistsSync.mockImplementation((p: string) => p === bunBin);

      const result = captureUserShellPath();
      expect(result).toContain(bunBin);
      // Should not attempt shell sourcing on Windows
      expect(mockExecSync).not.toHaveBeenCalled();
    },
  );
});

// ─── buildFallbackPath ──────────────────────────────────────────────────────

describe("buildFallbackPath", () => {
  it.skipIf(IS_WINDOWS)(
    "includes standard system paths when they exist",
    () => {
      mockExistsSync.mockImplementation((p: string) =>
        ["/usr/local/bin", "/usr/bin", "/bin"].includes(p as string),
      );

      const result = buildFallbackPath();
      expect(result).toContain("/usr/local/bin");
      expect(result).toContain("/usr/bin");
      expect(result).toContain("/bin");
    },
  );

  it.skipIf(!IS_WINDOWS)(
    "includes Windows system and program paths when they exist",
    () => {
      const systemRoot = process.env.SystemRoot || "C:\\Windows";
      const system32 = join(systemRoot, "System32");
      const programFiles = process.env.ProgramFiles || "C:\\Program Files";
      const nodejs = join(programFiles, "nodejs");
      const gitCmd = join(programFiles, "Git", "cmd");

      mockExistsSync.mockImplementation(
        (p: string) => p === system32 || p === nodejs || p === gitCmd,
      );

      const result = buildFallbackPath();
      expect(result).toContain(system32);
      expect(result).toContain(nodejs);
      expect(result).toContain(gitCmd);
    },
  );

  it.skipIf(!IS_WINDOWS)(
    "includes %APPDATA%\\npm for global node modules on Windows",
    () => {
      const appData = process.env.APPDATA || join(HOME, "AppData", "Roaming");
      const npmDir = join(appData, "npm");
      mockExistsSync.mockImplementation((p: string) => p === npmDir);

      const result = buildFallbackPath();
      expect(result).toContain(npmDir);
    },
  );

  it("includes ~/.local/bin for claude CLI", () => {
    const localBin = join(HOME, ".local", "bin");
    const systemDir = IS_WINDOWS
      ? join(process.env.SystemRoot || "C:\\Windows", "System32")
      : "/usr/bin";
    mockExistsSync.mockImplementation(
      (p: string) => p === localBin || p === systemDir,
    );

    const result = buildFallbackPath();
    expect(result).toContain(localBin);
  });

  it("includes ~/.bun/bin", () => {
    const bunBin = join(HOME, ".bun", "bin");
    const systemDir = IS_WINDOWS
      ? join(process.env.SystemRoot || "C:\\Windows", "System32")
      : "/usr/bin";
    mockExistsSync.mockImplementation(
      (p: string) => p === bunBin || p === systemDir,
    );

    const result = buildFallbackPath();
    expect(result).toContain(bunBin);
  });

  it("includes ~/.cargo/bin for Rust tools", () => {
    const cargoBin = join(HOME, ".cargo", "bin");
    const systemDir = IS_WINDOWS
      ? join(process.env.SystemRoot || "C:\\Windows", "System32")
      : "/usr/bin";
    mockExistsSync.mockImplementation(
      (p: string) => p === cargoBin || p === systemDir,
    );

    const result = buildFallbackPath();
    expect(result).toContain(cargoBin);
  });

  it.skipIf(IS_WINDOWS)(
    "probes nvm versions directory and includes all version bins",
    () => {
      delete process.env.NVM_DIR;
      mockExistsSync.mockImplementation((p: string) => {
        if (p === "/home/testuser/.nvm/versions/node") return true;
        if (p.includes(".nvm/versions/node/v") && p.endsWith("/bin")) return true;
        if (p === "/usr/bin") return true;
        return false;
      });
      mockReaddirSync.mockReturnValue(["v18.20.0", "v22.17.0"] as any);

      const result = buildFallbackPath();
      expect(result).toContain("/home/testuser/.nvm/versions/node/v18.20.0/bin");
      expect(result).toContain("/home/testuser/.nvm/versions/node/v22.17.0/bin");
    },
  );

  it.skipIf(IS_WINDOWS)(
    "uses NVM_DIR env var when set",
    () => {
      process.env.NVM_DIR = "/custom/nvm";
      mockExistsSync.mockImplementation((p: string) => {
        if (p === "/custom/nvm/versions/node") return true;
        if (p.includes("/custom/nvm/versions/node/v") && p.endsWith("/bin"))
          return true;
        return false;
      });
      mockReaddirSync.mockReturnValue(["v20.0.0"] as any);

      const result = buildFallbackPath();
      expect(result).toContain("/custom/nvm/versions/node/v20.0.0/bin");
    },
  );

  it("excludes directories that don't exist", () => {
    mockExistsSync.mockReturnValue(false);

    const result = buildFallbackPath();
    expect(result).toBe("");
  });

  it("deduplicates PATH entries", () => {
    mockExistsSync.mockReturnValue(true);
    mockReaddirSync.mockReturnValue([] as any);

    const result = buildFallbackPath();
    const dirs = result.split(delimiter);
    expect(dirs.length).toBe(new Set(dirs).size);
  });
});

// ─── getEnrichedPath ────────────────────────────────────────────────────────

describe("getEnrichedPath", () => {
  it.skipIf(IS_WINDOWS)(
    "merges user shell PATH with current process PATH",
    () => {
      process.env.PATH = "/usr/bin:/bin";
      mockExecSync.mockImplementation((cmd: string) => {
        if (typeof cmd === "string" && cmd.includes("-lic")) {
          return "___PATH_START___/usr/bin:/home/testuser/.cargo/bin___PATH_END___\n";
        }
        return "";
      });

      const result = getEnrichedPath();
      expect(result).toContain("/home/testuser/.cargo/bin");
      expect(result).toContain("/usr/bin");
      expect(result).toContain("/bin");
    },
  );

  it.skipIf(!IS_WINDOWS)(
    "merges fallback PATH with current process PATH on Windows",
    () => {
      const bunBin = join(HOME, ".bun", "bin");
      const cargoBin = join(HOME, ".cargo", "bin");
      process.env.PATH = "C:\\Windows\\System32;C:\\Windows";
      mockExistsSync.mockImplementation(
        (p: string) => p === bunBin || p === cargoBin,
      );

      const result = getEnrichedPath();
      expect(result).toContain(bunBin);
      expect(result).toContain("C:\\Windows\\System32");
    },
  );

  it.skipIf(IS_WINDOWS)(
    "deduplicates entries from both PATHs",
    () => {
      process.env.PATH = "/usr/bin:/bin:/usr/local/bin";
      mockExecSync.mockImplementation((cmd: string) => {
        if (typeof cmd === "string" && cmd.includes("-lic")) {
          return "___PATH_START___/usr/bin:/usr/local/bin:/home/testuser/.volta/bin___PATH_END___\n";
        }
        return "";
      });

      const result = getEnrichedPath();
      const dirs = result.split(delimiter);
      expect(dirs.length).toBe(new Set(dirs).size);
      // /usr/bin should appear exactly once
      expect(dirs.filter((d) => d === "/usr/bin").length).toBe(1);
    },
  );

  it("caches the result after first call", () => {
    if (IS_WINDOWS) {
      const bunBin = join(HOME, ".bun", "bin");
      process.env.PATH = "C:\\Windows\\System32";
      mockExistsSync.mockImplementation((p: string) => p === bunBin);
    } else {
      process.env.PATH = "/usr/bin";
      mockExecSync.mockImplementation((cmd: string) => {
        if (typeof cmd === "string" && cmd.includes("-lic")) {
          return "___PATH_START___/usr/bin___PATH_END___\n";
        }
        return "";
      });
    }

    const first = getEnrichedPath();
    mockExecSync.mockClear();
    mockExistsSync.mockClear();
    const second = getEnrichedPath();

    expect(first).toBe(second);
    if (!IS_WINDOWS) {
      // execSync should NOT be called again (result was cached)
      expect(mockExecSync).not.toHaveBeenCalled();
    }
  });

  it.skipIf(IS_WINDOWS)(
    "gives user shell PATH precedence over process PATH",
    () => {
      // User's shell has /opt/homebrew/bin first, process PATH has /usr/bin first
      process.env.PATH = "/usr/bin:/bin";
      mockExecSync.mockImplementation((cmd: string) => {
        if (typeof cmd === "string" && cmd.includes("-lic")) {
          return "___PATH_START___/opt/homebrew/bin:/usr/bin___PATH_END___\n";
        }
        return "";
      });

      const result = getEnrichedPath();
      const dirs = result.split(delimiter);
      expect(dirs.indexOf("/opt/homebrew/bin")).toBeLessThan(
        dirs.indexOf("/bin"),
      );
    },
  );
});

// ─── resolveBinary ──────────────────────────────────────────────────────────

describe("resolveBinary", () => {
  const LOOKUP_CMD = IS_WINDOWS ? "where" : "which";

  beforeEach(() => {
    // Seed getEnrichedPath cache to avoid shell-sourcing side effects
    if (IS_WINDOWS) {
      const bunBin = join(HOME, ".bun", "bin");
      process.env.PATH = "C:\\Windows\\System32";
      mockExistsSync.mockImplementation((p: string) => p === bunBin);
    } else {
      process.env.PATH = "/usr/bin:/bin";
      mockExecSync.mockImplementation((cmd: string) => {
        if (typeof cmd === "string" && cmd.includes("-lic")) {
          return "___PATH_START___/usr/bin:/usr/local/bin___PATH_END___\n";
        }
        throw new Error("not found");
      });
    }
  });

  it(`returns absolute path when binary is found via ${IS_WINDOWS ? "where" : "which"}`, () => {
    _resetPathCache();
    const expectedPath = IS_WINDOWS
      ? "C:\\Users\\testuser\\.local\\bin\\claude.exe"
      : "/home/testuser/.local/bin/claude";

    mockExecSync.mockImplementation((cmd: string) => {
      if (!IS_WINDOWS && typeof cmd === "string" && cmd.includes("-lic")) {
        return "___PATH_START___/usr/bin___PATH_END___\n";
      }
      if (typeof cmd === "string" && cmd.startsWith(`${LOOKUP_CMD} claude`)) {
        return `${expectedPath}\n`;
      }
      throw new Error("not found");
    });

    if (IS_WINDOWS) {
      const bunBin = join(HOME, ".bun", "bin");
      mockExistsSync.mockImplementation((p: string) => p === bunBin);
    }

    expect(resolveBinary("claude")).toBe(expectedPath);
  });

  it("returns null when binary is not found anywhere", () => {
    _resetPathCache();
    mockExecSync.mockImplementation((cmd: string) => {
      if (!IS_WINDOWS && typeof cmd === "string" && cmd.includes("-lic")) {
        return "___PATH_START___/usr/bin___PATH_END___\n";
      }
      throw new Error("not found");
    });

    if (IS_WINDOWS) {
      mockExistsSync.mockReturnValue(false);
    }

    expect(resolveBinary("nonexistent")).toBeNull();
  });

  it(`passes enriched PATH to ${IS_WINDOWS ? "where" : "which"} command`, () => {
    _resetPathCache();
    const specialBin = IS_WINDOWS
      ? join(HOME, ".special", "bin")
      : "/home/testuser/.special/bin";
    const toolPath = IS_WINDOWS
      ? join(specialBin, "mytool.exe")
      : "/home/testuser/.special/bin/mytool";

    mockExecSync.mockImplementation((cmd: string, opts?: any) => {
      if (!IS_WINDOWS && typeof cmd === "string" && cmd.includes("-lic")) {
        return `___PATH_START___/usr/bin:${specialBin}___PATH_END___\n`;
      }
      if (typeof cmd === "string" && cmd.startsWith(LOOKUP_CMD)) {
        // Verify enriched PATH is passed in env
        expect(opts?.env?.PATH).toContain(specialBin);
        return `${toolPath}\n`;
      }
      throw new Error("not found");
    });

    if (IS_WINDOWS) {
      mockExistsSync.mockImplementation((p: string) => p === specialBin);
      process.env.PATH = `C:\\Windows\\System32;${specialBin}`;
    }

    resolveBinary("mytool");
  });

  it("returns the path directly when given an absolute path that exists", () => {
    const absPath = IS_WINDOWS ? "C:\\opt\\bin\\claude.exe" : "/opt/bin/claude";
    mockExistsSync.mockReturnValue(true);
    expect(resolveBinary(absPath)).toBe(absPath);
  });

  it("returns null when given an absolute path that does not exist", () => {
    const absPath = IS_WINDOWS ? "C:\\nonexistent\\claude.exe" : "/nonexistent/claude";
    mockExistsSync.mockReturnValue(false);
    expect(resolveBinary(absPath)).toBeNull();
  });
});

// ─── getServicePath ─────────────────────────────────────────────────────────

describe("getServicePath", () => {
  it("returns the same value as getEnrichedPath", () => {
    if (IS_WINDOWS) {
      const bunBin = join(HOME, ".bun", "bin");
      process.env.PATH = "C:\\Windows\\System32";
      mockExistsSync.mockImplementation((p: string) => p === bunBin);
    } else {
      process.env.PATH = "/usr/bin";
      mockExecSync.mockImplementation((cmd: string) => {
        if (typeof cmd === "string" && cmd.includes("-lic")) {
          return "___PATH_START___/usr/bin:/opt/homebrew/bin___PATH_END___\n";
        }
        return "";
      });
    }

    expect(getServicePath()).toBe(getEnrichedPath());
  });
});

// ─── _resetPathCache ────────────────────────────────────────────────────────

describe("_resetPathCache", () => {
  it("clears the cached PATH so next call re-computes", () => {
    if (IS_WINDOWS) {
      const bunBin = join(HOME, ".bun", "bin");
      const cargoBin = join(HOME, ".cargo", "bin");
      process.env.PATH = "C:\\Windows\\System32";
      let callCount = 0;
      mockExistsSync.mockImplementation((p: string) => {
        if (callCount === 0) return p === bunBin;
        return p === cargoBin;
      });

      const first = getEnrichedPath();
      _resetPathCache();
      callCount = 1;
      const second = getEnrichedPath();

      expect(first).not.toBe(second);
    } else {
      process.env.PATH = "/usr/bin";
      let callCount = 0;
      mockExecSync.mockImplementation((cmd: string) => {
        if (typeof cmd === "string" && cmd.includes("-lic")) {
          callCount++;
          return `___PATH_START___/usr/bin:/call-${callCount}___PATH_END___\n`;
        }
        return "";
      });

      const first = getEnrichedPath();
      _resetPathCache();
      const second = getEnrichedPath();

      expect(first).not.toBe(second);
      expect(first).toContain("/call-1");
      expect(second).toContain("/call-2");
    }
  });
});
