import fs from "node:fs"
import path from "node:path"

import { describe, expect, test } from "vitest"

// @ts-expect-error no typings available
import { changed, diff as myersDiff } from "myers-diff"
import { streamDiff } from "../diff/streamDiff.js"
import { DiffLine, DiffType } from "../index.js"
import { generateLines } from "./util.js"

// "modification" is an extra type used to represent an "old" + "new" diff line
type MyersDiffTypes = Extract<DiffType, "new" | "old"> | "modification"

const UNIFIED_DIFF_SYMBOLS = {
  same: "",
  new: "+",
  old: "-",
}

async function collectDiffs(
  oldLines: string[],
  newLines: string[],
): Promise<{ streamDiffs: DiffLine[]; myersDiffs: any }> {
  const streamDiffs: DiffLine[] = []

  for await (const diffLine of streamDiff(oldLines, generateLines(newLines))) {
    streamDiffs.push(diffLine)
  }

  const myersDiffs = myersDiff(oldLines.join("\n"), newLines.join("\n"))

  return { streamDiffs, myersDiffs }
}

function getMyersDiffType(diff: any): MyersDiffTypes | undefined {
  if (changed(diff.rhs) && !changed(diff.lhs)) {
    return "new"
  }

  if (!changed(diff.rhs) && changed(diff.lhs)) {
    return "old"
  }

  if (changed(diff.rhs) && changed(diff.lhs)) {
    return "modification"
  }

  return undefined
}

function displayDiff(diff: DiffLine[]) {
  return diff.map(({ type, line }) => `${UNIFIED_DIFF_SYMBOLS[type]} ${line}`).join("\n")
}

async function expectDiff(file: string) {
  const testFilePath = path.join(__dirname, "test-examples", file + ".diff")
  const testFileContents = fs.readFileSync(testFilePath, "utf-8")
  const normalized = testFileContents.replace(/\r\n/g, "\n")

  const [oldText, newText, expectedDiff] = normalized.split("\n---\n").map((s) => s.replace(/^\n+/, "").trimEnd())
  const oldLines = oldText.split("\n")
  const newLines = newText.split("\n")
  const { streamDiffs } = await collectDiffs(oldLines, newLines)
  const displayedDiff = displayDiff(streamDiffs)

  if (!expectedDiff || expectedDiff.trim() === "") {
    console.log("Expected diff was empty. Writing computed diff to the test file")
    // Persist with LF to keep fixtures stable cross-platform
    fs.writeFileSync(testFilePath, `${oldText}\n\n---\n\n${newText}\n\n---\n\n${displayedDiff}`)

    throw new Error("Expected diff is empty")
  }

  expect(displayedDiff).toEqual(expectedDiff)
}

// We use a longer `)` string here to not get
// caught by the fuzzy matcher
describe("streamDiff(", () => {
  test("no changes", async () => {
    const oldLines = ["first item", "second arg", "third param"]
    const newLines = ["first item", "second arg", "third param"]

    const { streamDiffs, myersDiffs } = await collectDiffs(oldLines, newLines)

    expect(streamDiffs).toEqual([
      { type: "same", line: "first item" },
      { type: "same", line: "second arg" },
      { type: "same", line: "third param" },
    ])

    expect(myersDiffs).toEqual([])
  })

  test("add new line", async () => {
    const oldLines = ["first item", "second arg"]
    const newLines = ["first item", "second arg", "third param"]

    const { streamDiffs, myersDiffs } = await collectDiffs(oldLines, newLines)

    expect(streamDiffs).toEqual([
      { type: "same", line: "first item" },
      { type: "same", line: "second arg" },
      { type: "new", line: "third param" },
    ])

    expect(myersDiffs.length).toEqual(1)
    expect(getMyersDiffType(myersDiffs[0])).toBe("new")
  })

  test("remove line", async () => {
    const oldLines = ["first item", "second arg", "third param"]
    const newLines = ["first item", "third param"]

    const { streamDiffs, myersDiffs } = await collectDiffs(oldLines, newLines)

    expect(streamDiffs).toEqual([
      { type: "same", line: "first item" },
      { type: "old", line: "second arg" },
      { type: "same", line: "third param" },
    ])

    expect(myersDiffs.length).toEqual(1)
    expect(getMyersDiffType(myersDiffs[0])).toBe("old")
  })

  test("modify line", async () => {
    const oldLines = ["first item", "second arg", "third param"]
    const newLines = ["first item", "modified second arg", "third param"]

    const { streamDiffs, myersDiffs } = await collectDiffs(oldLines, newLines)

    expect(streamDiffs).toEqual([
      { type: "same", line: "first item" },
      { type: "old", line: "second arg" },
      { type: "new", line: "modified second arg" },
      { type: "same", line: "third param" },
    ])

    expect(myersDiffs.length).toEqual(1)
    expect(getMyersDiffType(myersDiffs[0])).toBe("modification")
  })

  test("add multiple lines", async () => {
    const oldLines = ["first item", "fourth val"]
    const newLines = ["first item", "second arg", "third param", "fourth val"]

    const { streamDiffs, myersDiffs } = await collectDiffs(oldLines, newLines)

    expect(streamDiffs).toEqual([
      { type: "same", line: "first item" },
      { type: "new", line: "second arg" },
      { type: "new", line: "third param" },
      { type: "same", line: "fourth val" },
    ])

    // Multi-line addition
    expect(myersDiffs[0].rhs.add).toEqual(2)
    expect(getMyersDiffType(myersDiffs[0])).toBe("new")
  })

  test("remove multiple lines", async () => {
    const oldLines = ["first item", "second arg", "third param", "fourth val"]
    const newLines = ["first item", "fourth val"]

    const { streamDiffs, myersDiffs } = await collectDiffs(oldLines, newLines)

    expect(streamDiffs).toEqual([
      { type: "same", line: "first item" },
      { type: "old", line: "second arg" },
      { type: "old", line: "third param" },
      { type: "same", line: "fourth val" },
    ])

    // Multi-line deletion
    expect(myersDiffs[0].lhs.del).toEqual(2)
    expect(getMyersDiffType(myersDiffs[0])).toBe("old")
  })

  test("empty old lines", async () => {
    const oldLines: string[] = []
    const newLines = ["first item", "second arg"]

    const { streamDiffs, myersDiffs } = await collectDiffs(oldLines, newLines)

    expect(streamDiffs).toEqual([
      { type: "new", line: "first item" },
      { type: "new", line: "second arg" },
    ])

    // Multi-line addition
    expect(myersDiffs[0].rhs.add).toEqual(2)
    expect(getMyersDiffType(myersDiffs[0])).toBe("new")
  })

  test("empty new lines", async () => {
    const oldLines = ["first item", "second arg"]
    const newLines: string[] = []

    const { streamDiffs, myersDiffs } = await collectDiffs(oldLines, newLines)

    expect(streamDiffs).toEqual([
      { type: "old", line: "first item" },
      { type: "old", line: "second arg" },
    ])

    // Multi-line deletion
    expect(myersDiffs[0].lhs.del).toEqual(2)
    expect(getMyersDiffType(myersDiffs[0])).toBe("old")
  })

  test("tabs vs. spaces differences are ignored", async () => {
    await expectDiff("fastapi-tabs-vs-spaces.py")
  })

  test("trailing whitespaces should match ", async () => {
    const oldLines = ["first item  ", "second arg ", "third param  "]

    const newLines = ["first item", "second arg", "third param "]

    const { streamDiffs } = await collectDiffs(oldLines, newLines)

    expect(streamDiffs).toEqual([
      { type: "same", line: "first item  " },
      { type: "same", line: "second arg " },
      { type: "same", line: "third param  " },
    ])
  })

  //indentation and whitespace handling
  test.each([false, true])(
    "ignores indentation changes for sufficiently long lines (trailingWhitespace: %s)",
    async (trailingWhitespace) => {
      let oldLines = [" short", "   middle", " a long enough line", "  short2", "indented line", "final line"]

      const newLines = ["short", "middle", "a long enough line", "short2", " indented line", "final line"]

      if (trailingWhitespace) {
        oldLines = oldLines.map((line) => line + " ")
      }

      const { streamDiffs } = await collectDiffs(oldLines, newLines)
      const expected = trailingWhitespace
        ? [
            { type: "old", line: " short " },
            { type: "new", line: "short" },
            { type: "old", line: "   middle " },
            { type: "new", line: "middle" },
            { type: "same", line: " a long enough line " },
            { type: "same", line: "  short2 " },
            { type: "same", line: "indented line " },
            { type: "same", line: "final line " },
          ]
        : [
            { type: "old", line: " short" },
            { type: "new", line: "short" },
            { type: "old", line: "   middle" },
            { type: "new", line: "middle" },
            { type: "same", line: " a long enough line" },
            { type: "same", line: "  short2" },
            { type: "same", line: "indented line" },
            { type: "same", line: "final line" },
          ]

      expect(streamDiffs).toEqual(expected)
    },
  )

  test("preserves original lines for minor reindentation in simple block", async () => {
    const oldLines = ["if (checkValueOf(x)) {", "   doSomethingWith(x);", "}"]
    const newLines = ["if (checkValueOf(x)) {", "  doSomethingWith(x);", "}"]

    const { streamDiffs } = await collectDiffs(oldLines, newLines)

    expect(streamDiffs).toEqual([
      { type: "same", line: "if (checkValueOf(x)) {" },
      { type: "same", line: "   doSomethingWith(x);" },
      { type: "same", line: "}" },
    ])
  })

  test("uses new lines for nested reindentation changes", async () => {
    const oldLines = ["if (checkValueOf(x)) {", "   doSomethingWith(x);", "}"]
    const newLines = [
      "if (checkValueOf(x)) {",
      "   if (reallyCheckValueOf(x)) {",
      "     doSomethingElseWith(x);",
      "   }",
      "}",
    ]

    const { streamDiffs } = await collectDiffs(oldLines, newLines)

    expect(streamDiffs).toEqual([
      { type: "same", line: "if (checkValueOf(x)) {" },
      { type: "new", line: "   if (reallyCheckValueOf(x)) {" },
      { type: "old", line: "   doSomethingWith(x);" },
      { type: "new", line: "     doSomethingElseWith(x);" },
      { type: "old", line: "}" },
      { type: "new", line: "   }" },
      { type: "new", line: "}" },
    ])
  })

  test("FastAPI example", async () => {
    await expectDiff("fastapi.py")
  })

  test("FastAPI comments", async () => {
    await expectDiff("add-comments.py")
  })

  test("Mock LLM example", async () => {
    await expectDiff("mock-llm.ts")
  })
})
