import { describe, expect, it } from "vitest";

import type { SkillMetadata } from "../../../../../src/domain/catalog/metadata";
import type { CodexInput } from "../../../../../src/domain/chat/input";
import { emptyComposerContextReferences } from "../../../../../src/features/chat/application/composer/context-references";
import {
  preparedUserInputWithWikiLinkMentionsSkillsAndContext,
  type WikiLinkMentionResolver,
} from "../../../../../src/features/chat/application/composer/wikilink-context";

const obsidianContext = (...sections: string[]) => ({
  type: "additionalContext" as const,
  key: "codex_panel_obsidian_context",
  kind: "untrusted" as const,
  value: ["Obsidian context for the current user input:", ...sections].join("\n"),
});

const wikilinkContext = (...mappings: string[]) => obsidianContext("Resolved wikilinks:", ...mappings);

function userInputWithWikiLinkMentionsAndSkills(
  text: string,
  resolveMention: WikiLinkMentionResolver,
  skills: readonly SkillMetadata[],
): CodexInput {
  return preparedUserInputWithWikiLinkMentionsSkillsAndContext(text, resolveMention, skills, emptyComposerContextReferences(), {
    referenceActiveNoteOnSend: false,
  }).input;
}

describe("wikilink context", () => {
  it("parses aliases, subpaths, and duplicate links", () => {
    const text = "See [[Alpha|label]], [[Beta#Heading]], [[Gamma^block]], and [[Alpha]].";
    const input = userInputWithWikiLinkMentionsAndSkills(text, (target) => ({ name: target, path: `${target}.md` }), []);

    expect(input).toEqual([
      { type: "text", text },
      { type: "mention", name: "Alpha", path: "Alpha.md" },
      { type: "mention", name: "Beta", path: "Beta.md" },
      { type: "mention", name: "Gamma", path: "Gamma.md" },
      wikilinkContext("- [[Alpha|label]] -> Alpha.md", "- [[Beta#Heading]] -> Beta.md", "- [[Gamma^block]] -> Gamma.md"),
    ]);
  });

  it("adds only resolved file mentions without changing the visible prompt body", () => {
    const text = "Please compare [[Alpha#Heading|A]] and [[Missing]].";
    const input = userInputWithWikiLinkMentionsAndSkills(
      text,
      (target) => (target === "Alpha" ? { name: "Alpha", path: "thoughts/Alpha.md" } : null),
      [],
    );

    expect(input).toEqual([
      { type: "text", text },
      { type: "mention", name: "Alpha", path: "thoughts/Alpha.md" },
      wikilinkContext("- [[Alpha#Heading|A]] -> thoughts/Alpha.md"),
    ]);
    expect(input).toHaveLength(3);
  });

  it("resolves aliases and subpaths from non-markdown wikilinks by target", () => {
    const text = "Open [[Bases/Projects.base|Projects]], [[References/Paper.pdf]], and [[Assets/Diagram.png#crop|Diagram]].";
    const input = userInputWithWikiLinkMentionsAndSkills(
      text,
      (target) => {
        const mentions = new Map([
          ["Bases/Projects.base", { name: "Projects", path: "Bases/Projects.base" }],
          ["References/Paper.pdf", { name: "Paper", path: "References/Paper.pdf" }],
          ["Assets/Diagram.png", { name: "Diagram", path: "Assets/Diagram.png" }],
        ]);
        return mentions.get(target) ?? null;
      },
      [],
    );
    expect(input).toEqual([
      { type: "text", text },
      { type: "mention", name: "Projects", path: "Bases/Projects.base" },
      { type: "mention", name: "Paper", path: "References/Paper.pdf" },
      { type: "mention", name: "Diagram", path: "Assets/Diagram.png" },
      wikilinkContext(
        "- [[Bases/Projects.base|Projects]] -> Bases/Projects.base",
        "- [[References/Paper.pdf]] -> References/Paper.pdf",
        "- [[Assets/Diagram.png#crop|Diagram]] -> Assets/Diagram.png",
      ),
    ]);
  });

  it("deduplicates mentions by resolved path", () => {
    const text = "Read [[Alpha]], [[Alpha#Heading]], and [[Alias|A]].";
    const input = userInputWithWikiLinkMentionsAndSkills(
      text,
      (target) => (target === "Alpha" || target === "Alias" ? { name: "Alpha", path: "thoughts/Alpha.md" } : null),
      [],
    );

    expect(input).toEqual([
      { type: "text", text },
      { type: "mention", name: "Alpha", path: "thoughts/Alpha.md" },
      wikilinkContext("- [[Alpha]] -> thoughts/Alpha.md"),
    ]);
  });

  it("adds resolved skill input without changing the visible prompt body", () => {
    const text = "Please use $obsidian-codex-panel-maintain with [[Alpha]].";
    const input = userInputWithWikiLinkMentionsAndSkills(
      text,
      (target) => (target === "Alpha" ? { name: "Alpha", path: "thoughts/Alpha.md" } : null),
      [
        {
          name: "obsidian-codex-panel-maintain",
          description: "Maintain Codex Panel",
          path: "/vault/___/skills/obsidian-codex-panel-maintain/SKILL.md",
          scope: "repo",
          enabled: true,
        } as never,
      ],
    );

    expect(input).toEqual([
      { type: "text", text },
      { type: "mention", name: "Alpha", path: "thoughts/Alpha.md" },
      {
        type: "skill",
        name: "obsidian-codex-panel-maintain",
        path: "/vault/___/skills/obsidian-codex-panel-maintain/SKILL.md",
      },
      wikilinkContext("- [[Alpha]] -> thoughts/Alpha.md"),
    ]);
  });

  it("ignores unresolved skills and deduplicates resolved skills by path", () => {
    const text = "Use $First, $missing, $first, and $Alias.";
    const input = userInputWithWikiLinkMentionsAndSkills(text, () => null, [
      {
        name: "First",
        description: "First skill",
        path: "/skills/first/SKILL.md",
        scope: "user",
        enabled: true,
      } as never,
      {
        name: "first",
        description: "Duplicate name should not win",
        path: "/skills/duplicate/SKILL.md",
        scope: "user",
        enabled: true,
      } as never,
      {
        name: "Alias",
        description: "Same path alias",
        path: "/skills/first/SKILL.md",
        scope: "user",
        enabled: true,
      } as never,
      {
        name: "missing",
        description: "Disabled skill",
        path: "/skills/missing/SKILL.md",
        scope: "user",
        enabled: false,
      } as never,
    ]);

    expect(input).toEqual([
      { type: "text", text },
      { type: "skill", name: "First", path: "/skills/first/SKILL.md" },
    ]);
  });

  it("leaves bare context references as raw text", () => {
    const text = "整理して @active and @selection";
    const prepared = preparedUserInputWithWikiLinkMentionsSkillsAndContext(
      text,
      (target) => (target === "notes/Alpha" ? { name: "Alpha", path: "notes/Alpha.md" } : null),
      [],
      {
        activeNote: { name: "Alpha", path: "notes/Alpha.md", linktext: "notes/Alpha" },
        selection: {
          name: "Alpha",
          path: "notes/Alpha.md",
          linktext: "notes/Alpha",
          range: { from: { line: 41, ch: 4 }, to: { line: 46, ch: 0 } },
          text: "selected text",
        },
      },
      { referenceActiveNoteOnSend: false },
    );

    expect(prepared.text).toBe(text);
    expect(prepared.input).toEqual([{ type: "text", text }]);
  });

  it("resolves completed active snapshots without depending on the current link context", () => {
    const prepared = preparedUserInputWithWikiLinkMentionsSkillsAndContext(
      "整理して [[Alpha]]",
      () => null,
      [],
      {
        activeNote: null,
        selection: null,
        activeNoteSnapshots: [{ name: "Alpha", path: "notes/Alpha.md", linktext: "Alpha" }],
      },
      { referenceActiveNoteOnSend: false },
    );

    expect(prepared.input).toContainEqual({ type: "mention", name: "Alpha", path: "notes/Alpha.md" });
  });

  it("references the active file on send when enabled without changing visible text", () => {
    const text = "Rewrite the introduction.";
    const prepared = preparedUserInputWithWikiLinkMentionsSkillsAndContext(
      text,
      () => null,
      [],
      {
        activeNote: { name: "Alpha", path: "notes/Alpha.md", linktext: "Alpha" },
        selection: null,
      },
      { referenceActiveNoteOnSend: true },
    );

    expect(prepared.text).toBe(text);
    expect(prepared.input).toEqual([
      { type: "text", text },
      { type: "mention", name: "<active>", path: "notes/Alpha.md" },
      {
        type: "additionalContext",
        key: "codex_panel_obsidian_context",
        kind: "untrusted",
        value: "Obsidian context for the current user input:\nReferenced active file:\n- <active> -> notes/Alpha.md",
      },
    ]);
  });

  it("keeps wikilinks and active file in one Obsidian context when both are present", () => {
    const text = "Compare [[Beta]] with the active file.";
    const prepared = preparedUserInputWithWikiLinkMentionsSkillsAndContext(
      text,
      (target) => (target === "Beta" ? { name: "Beta", path: "notes/Beta.md" } : null),
      [],
      {
        activeNote: { name: "Alpha", path: "notes/Alpha.md", linktext: "Alpha" },
        selection: null,
      },
      { referenceActiveNoteOnSend: true },
    );

    expect(prepared.text).toBe(text);
    expect(prepared.input).toEqual([
      { type: "text", text },
      { type: "mention", name: "Beta", path: "notes/Beta.md" },
      { type: "mention", name: "<active>", path: "notes/Alpha.md" },
      {
        type: "additionalContext",
        key: "codex_panel_obsidian_context",
        kind: "untrusted",
        value:
          "Obsidian context for the current user input:\nResolved wikilinks:\n- [[Beta]] -> notes/Beta.md\n\nReferenced active file:\n- <active> -> notes/Alpha.md",
      },
    ]);
  });

  it("keeps wikilinks, selections, and active file in one Obsidian context", () => {
    const text = "Compare [[Beta]] with [[Gamma]] (L2:C1-L2:C6).";
    const prepared = preparedUserInputWithWikiLinkMentionsSkillsAndContext(
      text,
      (target) => {
        if (target === "Beta") return { name: "Beta", path: "notes/Beta.md" };
        if (target === "Gamma") return { name: "Gamma", path: "notes/Gamma.md" };
        return null;
      },
      [],
      {
        activeNote: { name: "Alpha", path: "notes/Alpha.md", linktext: "Alpha" },
        selection: null,
        selectionSnapshots: [
          {
            name: "Gamma",
            path: "notes/Gamma.md",
            linktext: "Gamma",
            range: { from: { line: 1, ch: 0 }, to: { line: 1, ch: 5 } },
            text: "selected gamma",
          },
        ],
      },
      { referenceActiveNoteOnSend: true },
    );

    expect(prepared.input).toEqual([
      { type: "text", text },
      { type: "mention", name: "Beta", path: "notes/Beta.md" },
      { type: "mention", name: "Gamma", path: "notes/Gamma.md" },
      { type: "mention", name: "<active>", path: "notes/Alpha.md" },
      {
        type: "additionalContext",
        key: "codex_panel_obsidian_context",
        kind: "untrusted",
        value:
          "Obsidian context for the current user input:\nResolved wikilinks:\n- [[Beta]] -> notes/Beta.md\n- [[Gamma]] -> notes/Gamma.md\n\nReferenced selections:\n- [[Gamma]] (L2:C1-L2:C6) -> notes/Gamma.md L2:C1-L2:C6\n\n[[Gamma]] (L2:C1-L2:C6):\nselected gamma\n\nReferenced active file:\n- <active> -> notes/Alpha.md",
      },
    ]);
  });

  it("attaches completed selection snapshots without depending on the current editor selection", () => {
    const prepared = preparedUserInputWithWikiLinkMentionsSkillsAndContext(
      "整理して [[notes/Alpha]] (L42:C5-L47:C1)",
      (target) => (target === "notes/Alpha" ? { name: "Alpha", path: "notes/Alpha.md" } : null),
      [],
      {
        activeNote: null,
        selection: {
          name: "Beta",
          path: "notes/Beta.md",
          linktext: "notes/Beta",
          range: { from: { line: 0, ch: 0 }, to: { line: 0, ch: 4 } },
          text: "current selection",
        },
        selectionSnapshots: [
          {
            name: "Alpha",
            path: "notes/Alpha.md",
            linktext: "notes/Alpha",
            range: { from: { line: 41, ch: 4 }, to: { line: 46, ch: 0 } },
            text: "completed selection",
          },
        ],
      },
      { referenceActiveNoteOnSend: false },
    );

    expect(prepared.input).toContainEqual({
      type: "additionalContext",
      key: "codex_panel_obsidian_context",
      kind: "untrusted",
      value:
        "Obsidian context for the current user input:\nResolved wikilinks:\n- [[notes/Alpha]] -> notes/Alpha.md\n\nReferenced selections:\n- [[notes/Alpha]] (L42:C5-L47:C1) -> notes/Alpha.md L42:C5-L47:C1\n\n[[notes/Alpha]] (L42:C5-L47:C1):\ncompleted selection",
    });
  });
});
