import { join } from "node:path";

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

import {
  RULESYNC_AIIGNORE_FILE_NAME,
  RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
  RULESYNC_RELATIVE_DIR_PATH,
} from "../../constants/rulesync-paths.js";
import { setupTestDirectory } from "../../test-utils/test-directories.js";
import { ensureDir, writeFileContent } from "../../utils/file.js";
import { ClaudecodeIgnore } from "./claudecode-ignore.js";
import { RulesyncIgnore } from "./rulesync-ignore.js";

describe("ClaudecodeIgnore", () => {
  let testDir: string;
  let cleanup: () => Promise<void>;

  beforeEach(async () => {
    ({ testDir, cleanup } = await setupTestDirectory());
    vi.spyOn(process, "cwd").mockReturnValue(testDir);
  });

  afterEach(async () => {
    await cleanup();
    vi.restoreAllMocks();
  });

  describe("constructor", () => {
    it("should create instance with valid JSON content", () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(*.log)", "Read(node_modules/**)"],
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      expect(claudecodeIgnore).toBeInstanceOf(ClaudecodeIgnore);
      expect(claudecodeIgnore.getRelativeDirPath()).toBe(".claude");
      expect(claudecodeIgnore.getRelativeFilePath()).toBe("settings.json");
      expect(claudecodeIgnore.getPatterns()).toEqual(["Read(*.log)", "Read(node_modules/**)"]);
    });

    it("should handle empty permissions object", () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {},
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      expect(claudecodeIgnore.getPatterns()).toEqual([]);
    });

    it("should handle null deny array", () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: null,
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      expect(claudecodeIgnore.getPatterns()).toEqual([]);
    });

    it("should handle missing permissions property", () => {
      const jsonContent = JSON.stringify({}, null, 2);

      const claudecodeIgnore = new ClaudecodeIgnore({
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      expect(claudecodeIgnore.getPatterns()).toEqual([]);
    });

    it("should create instance with custom outputRoot", () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(*.tmp)"],
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        outputRoot: "/custom/path",
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      expect(claudecodeIgnore.getFilePath()).toBe("/custom/path/.claude/settings.json");
      expect(claudecodeIgnore.getPatterns()).toEqual(["Read(*.tmp)"]);
    });
  });

  describe("getSettablePaths", () => {
    it("should return correct paths for Claude Code", () => {
      const paths = ClaudecodeIgnore.getSettablePaths();

      expect(paths).toEqual({
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
      });
    });

    it("should default to shared settings.json when fileMode is omitted", () => {
      const paths = ClaudecodeIgnore.getSettablePaths({ options: {} });

      expect(paths.relativeFilePath).toBe("settings.json");
    });

    it("should resolve to settings.local.json when fileMode is 'local'", () => {
      const paths = ClaudecodeIgnore.getSettablePaths({ options: { fileMode: "local" } });

      expect(paths).toEqual({
        relativeDirPath: ".claude",
        relativeFilePath: "settings.local.json",
      });
    });

    it("should throw on unknown fileMode values rather than silently falling back", () => {
      expect(() => ClaudecodeIgnore.getSettablePaths({ options: { fileMode: "weird" } })).toThrow(
        /fileMode/,
      );
    });

    it("should accept explicit fileMode: 'shared'", () => {
      const paths = ClaudecodeIgnore.getSettablePaths({ options: { fileMode: "shared" } });

      expect(paths.relativeFilePath).toBe("settings.json");
    });

    it("should ignore unrelated keys in the options object", () => {
      const paths = ClaudecodeIgnore.getSettablePaths({
        options: { fileMode: "local", futureUnknown: 123 },
      });

      expect(paths.relativeFilePath).toBe("settings.local.json");
    });
  });

  describe("fromRulesyncIgnore with fileMode option", () => {
    it("should write to settings.local.json when fileMode is 'local'", async () => {
      const rulesyncIgnore = new RulesyncIgnore({
        relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
        relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
        fileContent: "*.log\nnode_modules/**",
      });

      const claudecodeIgnore = await ClaudecodeIgnore.fromRulesyncIgnore({
        outputRoot: testDir,
        rulesyncIgnore,
        options: { fileMode: "local" },
      });

      expect(claudecodeIgnore.getRelativeFilePath()).toBe("settings.local.json");
      expect(claudecodeIgnore.getFilePath()).toBe(join(testDir, ".claude", "settings.local.json"));

      const jsonValue = JSON.parse(claudecodeIgnore.getFileContent());
      expect(jsonValue.permissions.deny).toEqual(["Read(*.log)", "Read(node_modules/**)"]);
    });

    it("should not touch settings.json when writing to local mode", async () => {
      const sharedContent = JSON.stringify(
        { permissions: { deny: ["Read(shared.txt)"] } },
        null,
        2,
      );
      const claudeDir = join(testDir, ".claude");
      await ensureDir(claudeDir);
      await writeFileContent(join(claudeDir, "settings.json"), sharedContent);

      const rulesyncIgnore = new RulesyncIgnore({
        relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
        relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
        fileContent: "*.log",
      });

      const claudecodeIgnore = await ClaudecodeIgnore.fromRulesyncIgnore({
        outputRoot: testDir,
        rulesyncIgnore,
        options: { fileMode: "local" },
      });

      // The returned tool file targets settings.local.json, leaving the
      // existing shared settings.json untouched on disk.
      expect(claudecodeIgnore.getRelativeFilePath()).toBe("settings.local.json");
    });
  });

  describe("isDeletable", () => {
    it("should return false because settings.json is user-managed", () => {
      const claudecodeIgnore = new ClaudecodeIgnore({
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: JSON.stringify({ permissions: { deny: [] } }),
      });

      expect(claudecodeIgnore.isDeletable()).toBe(false);
    });
  });

  describe("toRulesyncIgnore", () => {
    it("should convert Read() patterns to plain patterns", () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(*.log)", "Read(node_modules/**)", "Read(.env)"],
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        outputRoot: testDir,
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      const rulesyncIgnore = claudecodeIgnore.toRulesyncIgnore();

      expect(rulesyncIgnore).toBeInstanceOf(RulesyncIgnore);
      expect(rulesyncIgnore.getFileContent()).toBe("*.log\nnode_modules/**\n.env");
      expect(rulesyncIgnore.getRelativeDirPath()).toBe(RULESYNC_RELATIVE_DIR_PATH);
      // When neither ignore file exists yet, Rulesync defaults to .rulesync/.aiignore
      expect(rulesyncIgnore.getRelativeFilePath()).toBe(RULESYNC_AIIGNORE_FILE_NAME);
    });

    it("should handle patterns without Read() wrapper", () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["*.log", "Read(node_modules/**)", ".env"],
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        outputRoot: testDir,
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      const rulesyncIgnore = claudecodeIgnore.toRulesyncIgnore();

      expect(rulesyncIgnore.getFileContent()).toBe("*.log\nnode_modules/**\n.env");
    });

    it("should filter out empty patterns", () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(*.log)", "Read()", "", "Read(node_modules/**)"],
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        outputRoot: testDir,
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      const rulesyncIgnore = claudecodeIgnore.toRulesyncIgnore();

      expect(rulesyncIgnore.getFileContent()).toBe("*.log\nnode_modules/**");
    });

    it("should handle empty deny array", () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: [],
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        outputRoot: testDir,
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      const rulesyncIgnore = claudecodeIgnore.toRulesyncIgnore();

      expect(rulesyncIgnore.getFileContent()).toBe("");
    });
  });

  describe("fromRulesyncIgnore", () => {
    it("should create ClaudecodeIgnore from RulesyncIgnore patterns", async () => {
      const fileContent = "*.log\nnode_modules/**\n.env";
      const rulesyncIgnore = new RulesyncIgnore({
        relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
        relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
        fileContent,
      });

      const claudecodeIgnore = await ClaudecodeIgnore.fromRulesyncIgnore({
        outputRoot: testDir,
        rulesyncIgnore,
      });

      expect(claudecodeIgnore).toBeInstanceOf(ClaudecodeIgnore);
      expect(claudecodeIgnore.getOutputRoot()).toBe(testDir);
      expect(claudecodeIgnore.getRelativeDirPath()).toBe(".claude");
      expect(claudecodeIgnore.getRelativeFilePath()).toBe("settings.json");

      const jsonValue = JSON.parse(claudecodeIgnore.getFileContent());
      expect(jsonValue.permissions.deny).toEqual([
        "Read(*.log)",
        "Read(.env)",
        "Read(node_modules/**)",
      ]);
    });

    it("should preserve non-Read permissions while syncing deny list", async () => {
      const existingJsonContent = JSON.stringify(
        {
          permissions: {
            allow: ["Write(*.js)"],
            deny: ["Write(secret.txt)", "Read(old.log)"],
          },
        },
        null,
        2,
      );

      const claudeDir = join(testDir, ".claude");
      await ensureDir(claudeDir);
      await writeFileContent(join(claudeDir, "settings.json"), existingJsonContent);

      const rulesyncIgnore = new RulesyncIgnore({
        relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
        relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
        fileContent: "*.log\nnode_modules/**",
      });

      const claudecodeIgnore = await ClaudecodeIgnore.fromRulesyncIgnore({
        outputRoot: testDir,
        rulesyncIgnore,
      });

      const jsonValue = JSON.parse(claudecodeIgnore.getFileContent());
      expect(jsonValue.permissions.deny).toEqual([
        "Read(*.log)",
        "Read(node_modules/**)",
        "Write(secret.txt)",
      ]);
      expect(jsonValue.permissions.allow).toEqual(["Write(*.js)"]);
    });

    it("should handle patterns with comments and empty lines", async () => {
      const fileContent = "# Comment\n*.log\n\nnode_modules/**\n# Another comment\n.env";
      const rulesyncIgnore = new RulesyncIgnore({
        relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
        relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
        fileContent,
      });

      const claudecodeIgnore = await ClaudecodeIgnore.fromRulesyncIgnore({
        outputRoot: testDir,
        rulesyncIgnore,
      });

      const jsonValue = JSON.parse(claudecodeIgnore.getFileContent());
      expect(jsonValue.permissions.deny).toEqual([
        "Read(*.log)",
        "Read(.env)",
        "Read(node_modules/**)",
      ]);
    });

    it("should preserve other JSON properties", async () => {
      const existingJsonContent = JSON.stringify(
        {
          other: "property",
          permissions: {
            allow: ["Write(*.js)"],
            deny: ["Read(secret.txt)"],
          },
          another: "value",
        },
        null,
        2,
      );

      const claudeDir = join(testDir, ".claude");
      await ensureDir(claudeDir);
      await writeFileContent(join(claudeDir, "settings.json"), existingJsonContent);

      const rulesyncIgnore = new RulesyncIgnore({
        relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
        relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
        fileContent: "*.log",
      });

      const claudecodeIgnore = await ClaudecodeIgnore.fromRulesyncIgnore({
        outputRoot: testDir,
        rulesyncIgnore,
      });

      const jsonValue = JSON.parse(claudecodeIgnore.getFileContent());
      expect(jsonValue.other).toBe("property");
      expect(jsonValue.another).toBe("value");
      expect(jsonValue.permissions.allow).toEqual(["Write(*.js)"]);
      expect(jsonValue.permissions.deny).toEqual(["Read(*.log)"]);
    });

    it("should remove stale Read entries when patterns are removed", async () => {
      const existingJsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(removed-entry)", "Write(secret.txt)", "Read(keep.log)"],
          },
        },
        null,
        2,
      );

      const claudeDir = join(testDir, ".claude");
      await ensureDir(claudeDir);
      await writeFileContent(join(claudeDir, "settings.json"), existingJsonContent);

      const rulesyncIgnore = new RulesyncIgnore({
        relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
        relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
        fileContent: "keep.log",
      });

      const claudecodeIgnore = await ClaudecodeIgnore.fromRulesyncIgnore({
        outputRoot: testDir,
        rulesyncIgnore,
      });

      const jsonValue = JSON.parse(claudecodeIgnore.getFileContent());
      expect(jsonValue.permissions.deny).toEqual(["Read(keep.log)", "Write(secret.txt)"]);
    });

    it("should remove duplicates and sort patterns", async () => {
      const existingJsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(*.log)", "Read(z.txt)", "Read(a.txt)"],
          },
        },
        null,
        2,
      );

      const claudeDir = join(testDir, ".claude");
      await ensureDir(claudeDir);
      await writeFileContent(join(claudeDir, "settings.json"), existingJsonContent);

      const rulesyncIgnore = new RulesyncIgnore({
        relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
        relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
        fileContent: "*.log\nb.txt\na.txt",
      });

      const claudecodeIgnore = await ClaudecodeIgnore.fromRulesyncIgnore({
        outputRoot: testDir,
        rulesyncIgnore,
      });

      const jsonValue = JSON.parse(claudecodeIgnore.getFileContent());
      expect(jsonValue.permissions.deny).toEqual(["Read(*.log)", "Read(a.txt)", "Read(b.txt)"]);
    });

    it("should handle Windows line endings", async () => {
      const fileContent = "*.log\r\nnode_modules/**\r\n.env";
      const rulesyncIgnore = new RulesyncIgnore({
        relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
        relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
        fileContent,
      });

      const claudecodeIgnore = await ClaudecodeIgnore.fromRulesyncIgnore({
        outputRoot: testDir,
        rulesyncIgnore,
      });

      const jsonValue = JSON.parse(claudecodeIgnore.getFileContent());
      expect(jsonValue.permissions.deny).toEqual([
        "Read(*.log)",
        "Read(.env)",
        "Read(node_modules/**)",
      ]);
    });

    it("should create new JSON file when none exists", async () => {
      const rulesyncIgnore = new RulesyncIgnore({
        relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
        relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
        fileContent: "*.log\nnode_modules/**",
      });

      const claudecodeIgnore = await ClaudecodeIgnore.fromRulesyncIgnore({
        outputRoot: testDir,
        rulesyncIgnore,
      });

      const jsonValue = JSON.parse(claudecodeIgnore.getFileContent());
      expect(jsonValue).toEqual({
        permissions: {
          deny: ["Read(*.log)", "Read(node_modules/**)"],
        },
      });
    });

    it("should use default outputRoot when not provided", async () => {
      const rulesyncIgnore = new RulesyncIgnore({
        relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
        relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
        fileContent: "*.tmp",
      });

      const claudecodeIgnore = await ClaudecodeIgnore.fromRulesyncIgnore({
        outputRoot: testDir,
        rulesyncIgnore,
      });

      expect(claudecodeIgnore.getOutputRoot()).toBe(testDir);

      const jsonValue = JSON.parse(claudecodeIgnore.getFileContent());
      expect(jsonValue.permissions.deny).toEqual(["Read(*.tmp)"]);
    });
  });

  describe("fromFile", () => {
    it("should read settings.json file from .claude directory", async () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(*.log)", "Read(node_modules/**)"],
          },
        },
        null,
        2,
      );

      const claudeDir = join(testDir, ".claude");
      await ensureDir(claudeDir);
      await writeFileContent(join(claudeDir, "settings.json"), jsonContent);

      const claudecodeIgnore = await ClaudecodeIgnore.fromFile({
        outputRoot: testDir,
      });

      expect(claudecodeIgnore).toBeInstanceOf(ClaudecodeIgnore);
      expect(claudecodeIgnore.getOutputRoot()).toBe(testDir);
      expect(claudecodeIgnore.getRelativeDirPath()).toBe(".claude");
      expect(claudecodeIgnore.getRelativeFilePath()).toBe("settings.json");
      expect(claudecodeIgnore.getPatterns()).toEqual(["Read(*.log)", "Read(node_modules/**)"]);
    });

    it("should read file with validation enabled by default", async () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(*.log)"],
          },
        },
        null,
        2,
      );

      const claudeDir = join(testDir, ".claude");
      await ensureDir(claudeDir);
      await writeFileContent(join(claudeDir, "settings.json"), jsonContent);

      const claudecodeIgnore = await ClaudecodeIgnore.fromFile({
        outputRoot: testDir,
      });

      expect(claudecodeIgnore.getFileContent()).toBe(jsonContent);
    });

    it("should read file with validation disabled", async () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(*.log)"],
          },
        },
        null,
        2,
      );

      const claudeDir = join(testDir, ".claude");
      await ensureDir(claudeDir);
      await writeFileContent(join(claudeDir, "settings.json"), jsonContent);

      const claudecodeIgnore = await ClaudecodeIgnore.fromFile({
        outputRoot: testDir,
        validate: false,
      });

      expect(claudecodeIgnore.getFileContent()).toBe(jsonContent);
    });

    it("should handle complex JSON structure", async () => {
      const jsonContent = JSON.stringify(
        {
          version: "1.0.0",
          permissions: {
            allow: ["Write(*.js)", "Execute(npm)"],
            deny: ["Read(*.log)", "Read(secrets/**)"],
          },
          customSettings: {
            theme: "dark",
          },
        },
        null,
        2,
      );

      const claudeDir = join(testDir, ".claude");
      await ensureDir(claudeDir);
      await writeFileContent(join(claudeDir, "settings.json"), jsonContent);

      const claudecodeIgnore = await ClaudecodeIgnore.fromFile({
        outputRoot: testDir,
      });

      expect(claudecodeIgnore.getFileContent()).toBe(jsonContent);
      expect(claudecodeIgnore.getPatterns()).toEqual(["Read(*.log)", "Read(secrets/**)"]);
    });

    it("should default outputRoot to process.cwd() when not provided", async () => {
      // process.cwd() is already mocked to return testDir in beforeEach
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(*.log)"],
          },
        },
        null,
        2,
      );

      const claudeDir = join(testDir, ".claude");
      await ensureDir(claudeDir);
      await writeFileContent(join(claudeDir, "settings.json"), jsonContent);

      const claudecodeIgnore = await ClaudecodeIgnore.fromFile({});

      expect(claudecodeIgnore.getOutputRoot()).toBe(testDir);
      expect(claudecodeIgnore.getPatterns()).toEqual(["Read(*.log)"]);
    });

    it("should throw error when shared settings file does not exist", async () => {
      await expect(
        ClaudecodeIgnore.fromFile({
          outputRoot: testDir,
        }),
      ).rejects.toThrow("File not found");
    });

    it("should fall back to empty settings when settings.local.json is missing", async () => {
      // The shared settings.json may exist on disk, but when fileMode is
      // "local" we must read settings.local.json (which doesn't exist yet)
      // and tolerate its absence rather than crashing.
      const claudeDir = join(testDir, ".claude");
      await ensureDir(claudeDir);
      await writeFileContent(
        join(claudeDir, "settings.json"),
        JSON.stringify({ permissions: { deny: ["Read(shared.txt)"] } }),
      );

      const claudecodeIgnore = await ClaudecodeIgnore.fromFile({
        outputRoot: testDir,
        options: { fileMode: "local" },
      });

      expect(claudecodeIgnore.getRelativeFilePath()).toBe("settings.local.json");
      expect(claudecodeIgnore.getPatterns()).toEqual([]);
    });

    it("should read settings.local.json when fileMode is 'local'", async () => {
      const claudeDir = join(testDir, ".claude");
      await ensureDir(claudeDir);
      await writeFileContent(
        join(claudeDir, "settings.local.json"),
        JSON.stringify({ permissions: { deny: ["Read(local-only.txt)"] } }),
      );

      const claudecodeIgnore = await ClaudecodeIgnore.fromFile({
        outputRoot: testDir,
        options: { fileMode: "local" },
      });

      expect(claudecodeIgnore.getRelativeFilePath()).toBe("settings.local.json");
      expect(claudecodeIgnore.getPatterns()).toEqual(["Read(local-only.txt)"]);
    });
  });

  describe("round-trip conversion", () => {
    it("should maintain patterns in round-trip conversion", async () => {
      const originalPatterns = ["*.log", "node_modules/**", ".env", "build/", "*.tmp"];
      const originalContent = originalPatterns.join("\n");

      const rulesyncIgnore = new RulesyncIgnore({
        relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
        relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
        fileContent: originalContent,
      });

      const claudecodeIgnore = await ClaudecodeIgnore.fromRulesyncIgnore({
        outputRoot: testDir,
        rulesyncIgnore,
      });

      const convertedRulesyncIgnore = claudecodeIgnore.toRulesyncIgnore();
      const convertedPatterns = convertedRulesyncIgnore.getFileContent().split("\n");
      const originalPatternsList = originalContent.split("\n");

      // Check that all original patterns are present (order may differ due to sorting)
      expect(convertedPatterns.toSorted()).toEqual(originalPatternsList.toSorted());
    });

    it("should handle complex patterns in round-trip", async () => {
      const patterns = [
        "*.log",
        "**/*.tmp",
        "!important.tmp",
        "node_modules/**",
        ".env*",
        "build/",
        "dist/**/*.map",
      ];
      const originalContent = patterns.join("\n");

      const rulesyncIgnore = new RulesyncIgnore({
        relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
        relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
        fileContent: originalContent,
      });

      const claudecodeIgnore = await ClaudecodeIgnore.fromRulesyncIgnore({
        outputRoot: testDir,
        rulesyncIgnore,
      });

      const convertedRulesyncIgnore = claudecodeIgnore.toRulesyncIgnore();
      const convertedPatterns = convertedRulesyncIgnore.getFileContent().split("\n");
      const originalPatternsList = originalContent.split("\n");

      // Check that all original patterns are present (order may differ due to sorting)
      expect(convertedPatterns.toSorted()).toEqual(originalPatternsList.toSorted());
    });
  });

  describe("edge cases", () => {
    it("should handle malformed JSON gracefully", () => {
      expect(() => {
        new ClaudecodeIgnore({
          relativeDirPath: ".claude",
          relativeFilePath: "settings.json",
          fileContent: "{ invalid json }",
        });
      }).toThrow();
    });

    it("should handle patterns with special characters", () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: [
              "Read(file with spaces.txt)",
              "Read(file(with)parens.txt)",
              "Read(file[with]brackets.txt)",
              "Read(file{with}braces.txt)",
            ],
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      const rulesyncIgnore = claudecodeIgnore.toRulesyncIgnore();

      expect(rulesyncIgnore.getFileContent()).toBe(
        "file with spaces.txt\n" +
          "file(with)parens.txt\n" +
          "file[with]brackets.txt\n" +
          "file{with}braces.txt",
      );
    });

    it("should handle nested Read() patterns", () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(Read(nested).txt)"],
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      const rulesyncIgnore = claudecodeIgnore.toRulesyncIgnore();

      expect(rulesyncIgnore.getFileContent()).toBe("Read(nested).txt");
    });

    it("should handle patterns with trailing/leading spaces", () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["  Read(*.log)  ", "Read(  spaced.txt  )"],
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      expect(claudecodeIgnore.getPatterns()).toEqual(["  Read(*.log)  ", "Read(  spaced.txt  )"]);
    });

    it("should handle unicode characters in patterns", () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(文件.log)", "Read(ファイル/**)", "Read(🚀.txt)"],
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      const rulesyncIgnore = claudecodeIgnore.toRulesyncIgnore();

      expect(rulesyncIgnore.getFileContent()).toBe("文件.log\nファイル/**\n🚀.txt");
    });

    it("should handle very long pattern lists", () => {
      const patterns = Array.from({ length: 100 }, (_, i) => `Read(file${i}.txt)`);
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: patterns,
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      expect(claudecodeIgnore.getPatterns()).toHaveLength(100);
      expect(claudecodeIgnore.getPatterns()[0]).toBe("Read(file0.txt)");
      expect(claudecodeIgnore.getPatterns()[99]).toBe("Read(file99.txt)");
    });
  });

  describe("inheritance from ToolIgnore", () => {
    it("should inherit getPatterns method", () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(*.log)", "Read(node_modules/**)", "Read(.env)"],
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      const patterns = claudecodeIgnore.getPatterns();

      expect(Array.isArray(patterns)).toBe(true);
      expect(patterns).toEqual(["Read(*.log)", "Read(node_modules/**)", "Read(.env)"]);
    });

    it("should inherit validation method", () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(*.log)"],
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      const result = claudecodeIgnore.validate();

      expect(result.success).toBe(true);
      expect(result.error).toBe(null);
    });

    it("should inherit file path methods from ToolFile", () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(*.log)"],
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        outputRoot: "/test/base",
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      expect(claudecodeIgnore.getOutputRoot()).toBe("/test/base");
      expect(claudecodeIgnore.getRelativeDirPath()).toBe(".claude");
      expect(claudecodeIgnore.getRelativeFilePath()).toBe("settings.json");
      expect(claudecodeIgnore.getFilePath()).toBe("/test/base/.claude/settings.json");
      expect(claudecodeIgnore.getFileContent()).toBe(jsonContent);
    });
  });

  describe("file integration", () => {
    it("should write and read file correctly", async () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(*.log)", "Read(node_modules/**)"],
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        outputRoot: testDir,
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      const claudeDir = join(testDir, ".claude");
      await ensureDir(claudeDir);
      await writeFileContent(claudecodeIgnore.getFilePath(), claudecodeIgnore.getFileContent());

      const readClaudecodeIgnore = await ClaudecodeIgnore.fromFile({
        outputRoot: testDir,
      });

      expect(readClaudecodeIgnore.getFileContent()).toBe(jsonContent);
      expect(readClaudecodeIgnore.getPatterns()).toEqual(["Read(*.log)", "Read(node_modules/**)"]);
    });

    it("should handle subdirectory placement", async () => {
      const subDir = join(testDir, "project");
      await ensureDir(subDir);

      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(*.log)"],
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        outputRoot: subDir,
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      const claudeDir = join(subDir, ".claude");
      await ensureDir(claudeDir);
      await writeFileContent(claudecodeIgnore.getFilePath(), claudecodeIgnore.getFileContent());

      const readClaudecodeIgnore = await ClaudecodeIgnore.fromFile({
        outputRoot: subDir,
      });

      expect(readClaudecodeIgnore.getFileContent()).toBe(jsonContent);
    });
  });

  describe("ClaudecodeIgnore-specific behavior", () => {
    it("should use .claude/settings.json as the file path", () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(*.log)"],
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      expect(claudecodeIgnore.getRelativeDirPath()).toBe(".claude");
      expect(claudecodeIgnore.getRelativeFilePath()).toBe("settings.json");
    });

    it("should format JSON output with 2-space indentation", async () => {
      const rulesyncIgnore = new RulesyncIgnore({
        relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
        relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
        fileContent: "*.log",
      });

      const claudecodeIgnore = await ClaudecodeIgnore.fromRulesyncIgnore({
        outputRoot: testDir,
        rulesyncIgnore,
      });

      const fileContent = claudecodeIgnore.getFileContent();
      const jsonValue = JSON.parse(fileContent);

      // Check that the JSON has the expected structure
      expect(jsonValue.permissions).toBeDefined();
      expect(jsonValue.permissions.deny).toEqual(["Read(*.log)"]);

      // Check that the file is formatted with proper indentation
      expect(fileContent).toContain('  "'); // Should have 2-space indentation
      expect(fileContent.startsWith("{")).toBe(true);
      expect(fileContent.endsWith("}")).toBe(true);
    });

    it("should handle Read() permission format correctly", () => {
      const jsonContent = JSON.stringify(
        {
          permissions: {
            deny: ["Read(**/*.secret)", "Read(private/**)", "Read(*.key)"],
          },
        },
        null,
        2,
      );

      const claudecodeIgnore = new ClaudecodeIgnore({
        relativeDirPath: ".claude",
        relativeFilePath: "settings.json",
        fileContent: jsonContent,
      });

      const patterns = claudecodeIgnore.getPatterns();
      expect(patterns).toEqual(["Read(**/*.secret)", "Read(private/**)", "Read(*.key)"]);

      const rulesyncIgnore = claudecodeIgnore.toRulesyncIgnore();
      expect(rulesyncIgnore.getFileContent()).toBe("**/*.secret\nprivate/**\n*.key");
    });
  });
});
