import { describe, expect, it } from "vitest"
import {
  computePageTitle,
  contentHash,
  detectBlockType,
  escapeHtml,
  extractLinks,
  extractProperties,
  extractTaskState,
  extractTitle,
  toRelativePath,
} from "./parse"

describe("contentHash", () => {
  it("returns a deterministic hash for the same content", () => {
    expect(contentHash("hello")).toBe(contentHash("hello"))
  })

  it("returns different hashes for different content", () => {
    expect(contentHash("hello")).not.toBe(contentHash("world"))
  })

  it("handles empty string", () => {
    expect(contentHash("")).toBe("0")
  })
})

describe("detectBlockType", () => {
  it("classifies heading", () => {
    expect(detectBlockType("# Title")).toBe("heading")
    expect(detectBlockType("## Subtitle")).toBe("heading")
    expect(detectBlockType("###### H6")).toBe("heading")
  })

  it("classifies task states", () => {
    expect(detectBlockType("TODO buy milk")).toBe("task")
    expect(detectBlockType("DOING implement feature")).toBe("task")
    expect(detectBlockType("DONE ship it")).toBe("task")
    expect(detectBlockType("LATER maybe")).toBe("task")
    expect(detectBlockType("NOW urgent")).toBe("task")
    expect(detectBlockType("WAITING on review")).toBe("task")
    expect(detectBlockType("CANCELLED wontfix")).toBe("task")
  })

  it("classifies callout", () => {
    expect(detectBlockType("> [!NOTE] info")).toBe("callout")
    expect(detectBlockType("> [!WARNING] caution")).toBe("callout")
  })

  it("classifies blockquote", () => {
    expect(detectBlockType("> quoted text")).toBe("blockquote")
  })

  it("classifies code fence", () => {
    expect(detectBlockType("```typescript")).toBe("code")
    expect(detectBlockType("```")).toBe("code")
  })

  it("classifies table", () => {
    expect(detectBlockType("| A | B |")).toBe("table")
  })

  it("classifies math block", () => {
    expect(detectBlockType("$$")).toBe("math")
    expect(detectBlockType("$$ E = mc^2 $$")).toBe("math")
  })

  it("classifies property", () => {
    expect(detectBlockType("author:: John")).toBe("property")
    expect(detectBlockType("date:: 2026-06-21")).toBe("property")
  })

  it("classifies plain text as paragraph", () => {
    expect(detectBlockType("some text")).toBe("paragraph")
    expect(detectBlockType("")).toBe("paragraph")
  })
})

describe("extractTitle", () => {
  it("extracts the first H1", () => {
    expect(extractTitle("# Hello\n\nSome text")).toBe("Hello")
  })

  it("returns empty string when no H1", () => {
    expect(extractTitle("## Not H1\n\nText")).toBe("")
  })

  it("returns empty string for empty content", () => {
    expect(extractTitle("")).toBe("")
  })
})

describe("computePageTitle", () => {
  it("uses # Heading 1 when present", () => {
    expect(computePageTitle("# My Title\n\ncontent\n", "pages/Foo.md")).toBe(
      "My Title",
    )
  })

  it("falls back to filename stem when there is no heading", () => {
    expect(computePageTitle("plain text\n", "pages/My Page.md")).toBe("My Page")
  })

  it("handles subdirectory paths", () => {
    expect(computePageTitle("content", "projects/sub/deep/Deep Note.md")).toBe(
      "Deep Note",
    )
  })

  it("returns filename stem for empty content", () => {
    expect(computePageTitle("", "pages/Empty.md")).toBe("Empty")
  })

  it("strips .md extension (case-insensitive)", () => {
    expect(computePageTitle("", "pages/Readme.MD")).toBe("Readme")
  })

  it("preserves underscores and dashes in the stem", () => {
    expect(computePageTitle("", "pages/my_notes-file.md")).toBe("my_notes-file")
  })

  it("uses heading over filename stem when both exist", () => {
    const result = computePageTitle(
      "# Actual Title\n\ncontent\n",
      "pages/Other Name.md",
    )
    expect(result).toBe("Actual Title")
    expect(result).not.toBe("Other Name")
  })
})

describe("extractLinks", () => {
  it("extracts [[Page Name]] references", () => {
    const links = extractLinks("see [[Other Page]] for details")
    expect(links).toHaveLength(1)
    expect(links[0]).toMatchObject({
      target: "Other Page",
      linkType: "page-ref",
    })
  })

  describe("toRelativePath", () => {
    it("strips workspace prefix from absolute path", () => {
      expect(
        toRelativePath(
          "/home/user/workspace/pages/Foo.md",
          "/home/user/workspace",
        ),
      ).toBe("pages/Foo.md")
    })

    it("handles workspace path with trailing slash", () => {
      expect(
        toRelativePath(
          "/home/user/workspace/journals/2026-01-01.md",
          "/home/user/workspace/",
        ),
      ).toBe("journals/2026-01-01.md")
    })

    it("returns already-relative path unchanged", () => {
      expect(toRelativePath("pages/Foo.md", "/home/user/workspace")).toBe(
        "pages/Foo.md",
      )
    })

    it("handles deeply nested paths", () => {
      expect(
        toRelativePath(
          "/Users/test/workspace/pages/sub/deep/file.md",
          "/Users/test/workspace",
        ),
      ).toBe("pages/sub/deep/file.md")
    })

    it("does not strip prefix from unrelated path with matching start", () => {
      expect(
        toRelativePath(
          "/home/user/workspace-other/file.md",
          "/home/user/workspace",
        ),
      ).toBe("/home/user/workspace-other/file.md")
    })
  })

  it("extracts [[Page|Alias]] references", () => {
    const links = extractLinks("see [[Page|Alias]] here")
    expect(links).toHaveLength(1)
    expect(links[0]).toMatchObject({ target: "Page", linkType: "page-ref" })
  })

  it("extracts ((block-id)) references", () => {
    const links = extractLinks("ref ((abc123)) here")
    expect(links).toHaveLength(1)
    expect(links[0]).toMatchObject({ target: "abc123", linkType: "block-ref" })
  })

  it("extracts #tag references", () => {
    const links = extractLinks("tagged #project-alpha here")
    expect(links).toHaveLength(1)
    expect(links[0]).toMatchObject({ target: "project-alpha", linkType: "tag" })
  })

  it("lowercases tag targets", () => {
    const links = extractLinks("#Project-Alpha")
    expect(links[0]?.target).toBe("project-alpha")
  })

  it("extracts multiple link types from mixed content", () => {
    const content = "Page [[Foo]] and ((id-123)) and #tag"
    const links = extractLinks(content)
    expect(links).toHaveLength(3)
    expect(links.map((l) => l.linkType).sort()).toEqual([
      "block-ref",
      "page-ref",
      "tag",
    ])
  })

  it("records correct offset positions", () => {
    const content = "text [[Foo]] text"
    const links = extractLinks(content)
    expect(links[0]?.offset).toBe(5) // [[Foo]] starts at position 5
  })

  it("handles content with no links", () => {
    expect(extractLinks("plain text")).toHaveLength(0)
  })
})

describe("escapeHtml", () => {
  it("escapes & < > \" '", () => {
    expect(escapeHtml(`&<>"'`)).toBe("&amp;&lt;&gt;&quot;&#039;")
  })

  it("passes plain text through unchanged", () => {
    expect(escapeHtml("hello world")).toBe("hello world")
  })

  it("passes [[markdown links]] through unchanged", () => {
    expect(escapeHtml("see [[Other Page]]")).toBe("see [[Other Page]]")
  })

  it("escapes html in excerpt before mark highlighting", () => {
    const safe = escapeHtml("<img src=x onerror=alert(1)>")
    const result = safe.replace(
      /\[\[([^\]]+)\]\]/g,
      '<mark class="hl">$1</mark>',
    )
    expect(result).toBe("&lt;img src=x onerror=alert(1)&gt;")
  })

  it("escapes html mixed with [[links]]", () => {
    const safe = escapeHtml("<script>alert(1)</script> and [[Page]]")
    const result = safe.replace(
      /\[\[([^\]]+)\]\]/g,
      '<mark class="hl">$1</mark>',
    )
    expect(result).toBe(
      '&lt;script&gt;alert(1)&lt;/script&gt; and <mark class="hl">Page</mark>',
    )
  })
})

describe("extractProperties", () => {
  it("extracts a simple key-value property", () => {
    expect(extractProperties("status:: active")).toEqual([
      { key: "status", value: "active" },
    ])
  })

  it("trims whitespace around the value", () => {
    expect(extractProperties("priority::  high  ")).toEqual([
      { key: "priority", value: "high" },
    ])
  })

  it("extracts hyphenated keys", () => {
    expect(extractProperties("due-date:: 2026-07-01")).toEqual([
      { key: "due-date", value: "2026-07-01" },
    ])
  })

  it("extracts underscored keys", () => {
    expect(extractProperties("project_name:: enesis")).toEqual([
      { key: "project_name", value: "enesis" },
    ])
  })

  it("skips lines without property syntax", () => {
    expect(extractProperties("just a normal line")).toEqual([])
  })

  it("skips lines where colons are mid-text", () => {
    expect(extractProperties("not a :: property")).toEqual([])
  })

  it("extracts property from the first line of multi-line content", () => {
    const result = extractProperties("status:: active\nSome description text")
    expect(result).toEqual([{ key: "status", value: "active" }])
  })

  it("extracts multiple properties from multiple lines", () => {
    const result = extractProperties(
      "priority:: high\nstatus:: active\ndue:: 2026-07-01",
    )
    expect(result).toEqual([
      { key: "priority", value: "high" },
      { key: "status", value: "active" },
      { key: "due", value: "2026-07-01" },
    ])
  })

  it("handles empty value after ::", () => {
    expect(extractProperties("empty::")).toEqual([{ key: "empty", value: "" }])
  })

  it("handles empty content", () => {
    expect(extractProperties("")).toEqual([])
  })
})

describe("extractTaskState", () => {
  it("detects TODO", () => {
    expect(extractTaskState("TODO buy milk")).toBe("TODO")
    expect(extractTaskState("DOING implement")).toBe("DOING")
    expect(extractTaskState("DONE ship it")).toBe("DONE")
    expect(extractTaskState("LATER maybe")).toBe("LATER")
    expect(extractTaskState("NOW urgent")).toBe("NOW")
    expect(extractTaskState("WAITING on review")).toBe("WAITING")
    expect(extractTaskState("CANCELLED wontfix")).toBe("CANCELLED")
  })

  it("returns null for non-task content", () => {
    expect(extractTaskState("Just a paragraph")).toBeNull()
    expect(extractTaskState("# Heading")).toBeNull()
    expect(extractTaskState("> Blockquote")).toBeNull()
  })

  it("handles task state at start of line", () => {
    expect(extractTaskState("TODO  ")).toBe("TODO")
    expect(extractTaskState("TODO")).toBe("TODO")
  })
})
