import { Hono } from "hono";
import { describe, it, expect, vi, beforeEach } from "vitest";

/* ── Mocks ────────────────────────────────────────────────────────── */

const mockPathValidator = vi.hoisted(() => ({
  getAllowedRoots: vi.fn().mockReturnValue(["/allowed"]),
  validateFsAccess: vi.fn().mockReturnValue({ ok: true, resolved: "/allowed/test" }),
}));

const mockConfig = vi.hoisted(() => ({
  config: { packageRoot: "/pkg" },
}));

const mockReaddir = vi.hoisted(() => vi.fn().mockResolvedValue([]));
const mockReadFile = vi.hoisted(() => vi.fn().mockResolvedValue(""));
const mockStat = vi.hoisted(() => vi.fn().mockResolvedValue({ size: 100 }));
const mockWriteFile = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
const mockMkdir = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
const mockHomedir = vi.hoisted(() => vi.fn().mockReturnValue("/home/user"));
const mockExecFileSync = vi.hoisted(() => vi.fn().mockReturnValue(""));

vi.mock("./path-validator.js", () => mockPathValidator);
vi.mock("../config.js", () => mockConfig);
vi.mock("node:fs/promises", () => ({
  readdir: mockReaddir,
  readFile: mockReadFile,
  stat: mockStat,
  writeFile: mockWriteFile,
  mkdir: mockMkdir,
}));
vi.mock("node:os", () => ({
  homedir: mockHomedir,
}));
vi.mock("node:child_process", () => ({
  execFileSync: mockExecFileSync,
  execSync: vi.fn().mockReturnValue(""),
}));

/* ── App setup ────────────────────────────────────────────────────── */

import { registerFsRoutes, isSensitiveDotfile } from "./fs-routes.js";

let app: Hono;

beforeEach(() => {
  vi.restoreAllMocks();
  mockPathValidator.getAllowedRoots.mockReturnValue(["/allowed"]);
  mockPathValidator.validateFsAccess.mockReturnValue({ ok: true, resolved: "/allowed/test" });
  mockReaddir.mockResolvedValue([]);
  mockReadFile.mockResolvedValue("");
  mockStat.mockResolvedValue({ size: 100 });
  mockWriteFile.mockResolvedValue(undefined);
  mockMkdir.mockResolvedValue(undefined);
  mockHomedir.mockReturnValue("/home/user");
  mockExecFileSync.mockReturnValue("");
  app = new Hono();
  registerFsRoutes(app);
});

/* ── Helpers ──────────────────────────────────────────────────────── */

function req(method: string, path: string, body?: unknown) {
  const init: RequestInit = { method, headers: {} };
  if (body) {
    (init.headers as Record<string, string>)["content-type"] = "application/json";
    init.body = JSON.stringify(body);
  }
  return app.request(path, init);
}

/* ── Tests ────────────────────────────────────────────────────────── */

describe("GET /fs/list", () => {
  it("returns directory listing", async () => {
    mockReaddir.mockResolvedValue([
      { name: "src", isDirectory: () => true, isFile: () => false },
      { name: ".hidden", isDirectory: () => true, isFile: () => false },
      { name: "file.txt", isDirectory: () => false, isFile: () => true },
    ]);
    const res = await req("GET", "/fs/list?path=/allowed/test");
    expect(res.status).toBe(200);
    const body = await res.json();
    // Should only include non-hidden directories
    expect(body.dirs).toHaveLength(1);
    expect(body.dirs[0].name).toBe("src");
  });

  it("returns 403 when path is not allowed", async () => {
    mockPathValidator.validateFsAccess.mockReturnValue({
      ok: false,
      error: "Access denied",
    });
    const res = await req("GET", "/fs/list?path=/forbidden");
    expect(res.status).toBe(403);
  });

  it("returns 400 on read error", async () => {
    mockReaddir.mockRejectedValue(new Error("ENOENT"));
    const res = await req("GET", "/fs/list?path=/allowed/test");
    expect(res.status).toBe(400);
  });
});

describe("GET /fs/home", () => {
  it("returns home directory", async () => {
    const res = await req("GET", "/fs/home");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.home).toBe("/home/user");
  });
});

describe("GET /fs/tree", () => {
  it("returns 400 when path is missing", async () => {
    const res = await req("GET", "/fs/tree");
    expect(res.status).toBe(400);
  });

  it("returns 403 when path not allowed", async () => {
    mockPathValidator.validateFsAccess.mockReturnValue({
      ok: false,
      error: "Access denied",
    });
    const res = await req("GET", "/fs/tree?path=/forbidden");
    expect(res.status).toBe(403);
  });

  it("returns tree structure", async () => {
    mockReaddir.mockResolvedValue([
      { name: "file.ts", isDirectory: () => false, isFile: () => true },
    ]);
    const res = await req("GET", "/fs/tree?path=/allowed/test");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.tree).toHaveLength(1);
    expect(body.tree[0].name).toBe("file.ts");
    expect(body.tree[0].type).toBe("file");
  });
});

describe("GET /fs/read", () => {
  it("returns 400 when path is missing", async () => {
    const res = await req("GET", "/fs/read");
    expect(res.status).toBe(400);
  });

  it("returns 403 when path not allowed", async () => {
    mockPathValidator.validateFsAccess.mockReturnValue({
      ok: false,
      error: "Access denied",
    });
    const res = await req("GET", "/fs/read?path=/forbidden/file.txt");
    expect(res.status).toBe(403);
  });

  it("returns file content", async () => {
    mockReadFile.mockResolvedValue("file content here");
    const res = await req("GET", "/fs/read?path=/allowed/test/file.txt");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.content).toBe("file content here");
  });

  it("returns 413 for files larger than 2MB", async () => {
    mockStat.mockResolvedValue({ size: 3 * 1024 * 1024 });
    const res = await req("GET", "/fs/read?path=/allowed/test/large.bin");
    expect(res.status).toBe(413);
  });

  it("returns 404 when file not found", async () => {
    mockStat.mockRejectedValue(new Error("ENOENT"));
    const res = await req("GET", "/fs/read?path=/allowed/test/missing.txt");
    expect(res.status).toBe(404);
  });
});

describe("PUT /fs/write", () => {
  it("writes file content", async () => {
    const res = await req("PUT", "/fs/write", {
      path: "/allowed/test/file.txt",
      content: "new content",
    });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.ok).toBe(true);
  });

  it("returns 403 when path not allowed", async () => {
    mockPathValidator.validateFsAccess.mockReturnValue({
      ok: false,
      error: "Access denied",
    });
    const res = await req("PUT", "/fs/write", {
      path: "/forbidden/file.txt",
      content: "hack",
    });
    expect(res.status).toBe(403);
  });

  it("returns 500 on write error", async () => {
    mockWriteFile.mockRejectedValue(new Error("EACCES"));
    const res = await req("PUT", "/fs/write", {
      path: "/allowed/test/file.txt",
      content: "data",
    });
    expect(res.status).toBe(500);
  });

  it("returns 403 when writing to ~/.ssh/ directory", async () => {
    // Simulate validated path resolving to a file inside ~/.ssh/
    mockPathValidator.validateFsAccess.mockReturnValue({
      ok: true,
      resolved: "/home/user/.ssh/authorized_keys",
    });
    const res = await req("PUT", "/fs/write", {
      path: "/home/user/.ssh/authorized_keys",
      content: "ssh-rsa AAAA...",
    });
    expect(res.status).toBe(403);
    const body = await res.json();
    expect(body.error).toContain("sensitive dotfile");
  });

  it("returns 403 when writing to ~/.gnupg/ directory", async () => {
    mockPathValidator.validateFsAccess.mockReturnValue({
      ok: true,
      resolved: "/home/user/.gnupg/gpg.conf",
    });
    const res = await req("PUT", "/fs/write", {
      path: "/home/user/.gnupg/gpg.conf",
      content: "malicious",
    });
    expect(res.status).toBe(403);
    const body = await res.json();
    expect(body.error).toContain("sensitive dotfile");
  });

  it("returns 403 when writing to ~/.aws/ directory", async () => {
    mockPathValidator.validateFsAccess.mockReturnValue({
      ok: true,
      resolved: "/home/user/.aws/credentials",
    });
    const res = await req("PUT", "/fs/write", {
      path: "/home/user/.aws/credentials",
      content: "aws_secret_access_key=...",
    });
    expect(res.status).toBe(403);
    const body = await res.json();
    expect(body.error).toContain("sensitive dotfile");
  });

  it("returns 403 when writing to ~/.bashrc", async () => {
    mockPathValidator.validateFsAccess.mockReturnValue({
      ok: true,
      resolved: "/home/user/.bashrc",
    });
    const res = await req("PUT", "/fs/write", {
      path: "/home/user/.bashrc",
      content: "export EVIL=1",
    });
    expect(res.status).toBe(403);
    const body = await res.json();
    expect(body.error).toContain("sensitive dotfile");
  });

  it("returns 403 when writing to ~/.profile", async () => {
    mockPathValidator.validateFsAccess.mockReturnValue({
      ok: true,
      resolved: "/home/user/.profile",
    });
    const res = await req("PUT", "/fs/write", {
      path: "/home/user/.profile",
      content: "malicious",
    });
    expect(res.status).toBe(403);
  });

  it("returns 403 when writing to ~/.zshrc", async () => {
    mockPathValidator.validateFsAccess.mockReturnValue({
      ok: true,
      resolved: "/home/user/.zshrc",
    });
    const res = await req("PUT", "/fs/write", {
      path: "/home/user/.zshrc",
      content: "malicious",
    });
    expect(res.status).toBe(403);
  });

  it("returns 403 when writing to ~/.npmrc", async () => {
    mockPathValidator.validateFsAccess.mockReturnValue({
      ok: true,
      resolved: "/home/user/.npmrc",
    });
    const res = await req("PUT", "/fs/write", {
      path: "/home/user/.npmrc",
      content: "//registry.npmjs.org/:_authToken=STOLEN",
    });
    expect(res.status).toBe(403);
  });

  it("returns 403 when writing to ~/.env", async () => {
    mockPathValidator.validateFsAccess.mockReturnValue({
      ok: true,
      resolved: "/home/user/.env",
    });
    const res = await req("PUT", "/fs/write", {
      path: "/home/user/.env",
      content: "SECRET=leaked",
    });
    expect(res.status).toBe(403);
    const body = await res.json();
    expect(body.error).toContain("sensitive dotfile");
  });

  it("returns 403 when writing to ~/.env.local", async () => {
    mockPathValidator.validateFsAccess.mockReturnValue({
      ok: true,
      resolved: "/home/user/.env.local",
    });
    const res = await req("PUT", "/fs/write", {
      path: "/home/user/.env.local",
      content: "SECRET=leaked",
    });
    expect(res.status).toBe(403);
  });

  it("allows writing .env files inside project subdirectories", async () => {
    // .env inside a project (not home root) should be allowed
    mockPathValidator.validateFsAccess.mockReturnValue({
      ok: true,
      resolved: "/home/user/project/.env",
    });
    const res = await req("PUT", "/fs/write", {
      path: "/home/user/project/.env",
      content: "PORT=3000",
    });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.ok).toBe(true);
  });

  it("allows writing normal files in home directory", async () => {
    // Regular files in ~ should still be writable
    mockPathValidator.validateFsAccess.mockReturnValue({
      ok: true,
      resolved: "/home/user/notes.txt",
    });
    const res = await req("PUT", "/fs/write", {
      path: "/home/user/notes.txt",
      content: "hello",
    });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.ok).toBe(true);
  });
});

describe("GET /fs/diff", () => {
  it("returns 400 when path is missing", async () => {
    const res = await req("GET", "/fs/diff");
    expect(res.status).toBe(400);
  });

  it("returns 403 when path not allowed", async () => {
    mockPathValidator.validateFsAccess.mockReturnValue({
      ok: false,
      error: "Access denied",
    });
    const res = await req("GET", "/fs/diff?path=/forbidden/file.txt");
    expect(res.status).toBe(403);
  });

  it("returns diff output", async () => {
    mockExecFileSync
      .mockReturnValueOnce("/repo") // rev-parse --show-toplevel
      .mockReturnValueOnce("src/file.ts") // ls-files
      .mockReturnValueOnce("diff --git a/file b/file\n+added line"); // diff HEAD
    const res = await req("GET", "/fs/diff?path=/allowed/test/file.ts");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.diff).toContain("+added line");
  });
});

describe("GET /fs/changed-files", () => {
  it("returns 400 when cwd is missing", async () => {
    const res = await req("GET", "/fs/changed-files");
    expect(res.status).toBe(400);
  });

  it("returns 403 when path not allowed", async () => {
    mockPathValidator.validateFsAccess.mockReturnValue({
      ok: false,
      error: "Access denied",
    });
    const res = await req("GET", "/fs/changed-files?cwd=/forbidden");
    expect(res.status).toBe(403);
  });

  it("returns empty files on git error", async () => {
    mockExecFileSync.mockImplementation(() => {
      throw new Error("not a git repo");
    });
    const res = await req("GET", "/fs/changed-files?cwd=/allowed/test");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.files).toEqual([]);
  });
});

describe("GET /fs/claude-md", () => {
  it("returns 400 when cwd is missing", async () => {
    const res = await req("GET", "/fs/claude-md");
    expect(res.status).toBe(400);
  });

  it("returns 403 when path not allowed", async () => {
    mockPathValidator.validateFsAccess.mockReturnValue({
      ok: false,
      error: "Access denied",
    });
    const res = await req("GET", "/fs/claude-md?cwd=/forbidden");
    expect(res.status).toBe(403);
  });

  it("returns CLAUDE.md files", async () => {
    mockExecFileSync.mockImplementation(() => {
      throw new Error("not a git repo");
    });
    mockReadFile.mockResolvedValue("# CLAUDE.md content");
    const res = await req("GET", "/fs/claude-md?cwd=/allowed/test");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.cwd).toBe("/allowed/test");
  });
});

describe("PUT /fs/claude-md", () => {
  it("returns 400 for non-CLAUDE.md path", async () => {
    const res = await req("PUT", "/fs/claude-md", {
      path: "/allowed/test/README.md",
      content: "data",
    });
    expect(res.status).toBe(400);
  });
});

describe("isSensitiveDotfile", () => {
  const home = "/home/user";

  it("blocks files inside .ssh directory", () => {
    expect(isSensitiveDotfile("/home/user/.ssh/authorized_keys", home)).toBe(true);
    expect(isSensitiveDotfile("/home/user/.ssh/id_rsa", home)).toBe(true);
    expect(isSensitiveDotfile("/home/user/.ssh/config", home)).toBe(true);
  });

  it("blocks files inside .gnupg directory", () => {
    expect(isSensitiveDotfile("/home/user/.gnupg/gpg.conf", home)).toBe(true);
  });

  it("blocks files inside .aws directory", () => {
    expect(isSensitiveDotfile("/home/user/.aws/credentials", home)).toBe(true);
    expect(isSensitiveDotfile("/home/user/.aws/config", home)).toBe(true);
  });

  it("blocks .bashrc in home root", () => {
    expect(isSensitiveDotfile("/home/user/.bashrc", home)).toBe(true);
  });

  it("blocks .profile in home root", () => {
    expect(isSensitiveDotfile("/home/user/.profile", home)).toBe(true);
  });

  it("blocks .zshrc in home root", () => {
    expect(isSensitiveDotfile("/home/user/.zshrc", home)).toBe(true);
  });

  it("blocks .npmrc in home root", () => {
    expect(isSensitiveDotfile("/home/user/.npmrc", home)).toBe(true);
  });

  it("blocks .env in home root", () => {
    expect(isSensitiveDotfile("/home/user/.env", home)).toBe(true);
  });

  it("blocks .env.local and .env.production in home root", () => {
    expect(isSensitiveDotfile("/home/user/.env.local", home)).toBe(true);
    expect(isSensitiveDotfile("/home/user/.env.production", home)).toBe(true);
  });

  it("allows .env files inside project subdirectories", () => {
    // .env in a project subdir (not home root) should be allowed
    expect(isSensitiveDotfile("/home/user/project/.env", home)).toBe(false);
    expect(isSensitiveDotfile("/home/user/project/.env.local", home)).toBe(false);
  });

  it("allows .bashrc inside project subdirectories", () => {
    // .bashrc in a nested dir is not in home root
    expect(isSensitiveDotfile("/home/user/project/.bashrc", home)).toBe(false);
  });

  it("allows normal files in home directory", () => {
    expect(isSensitiveDotfile("/home/user/notes.txt", home)).toBe(false);
    expect(isSensitiveDotfile("/home/user/project/src/index.ts", home)).toBe(false);
  });

  it("allows files outside home directory entirely", () => {
    expect(isSensitiveDotfile("/opt/projects/.ssh/config", home)).toBe(false);
    expect(isSensitiveDotfile("/tmp/.env", home)).toBe(false);
  });

  it("does not match the home directory itself", () => {
    expect(isSensitiveDotfile("/home/user", home)).toBe(false);
  });
});
