import { describe, expect, test } from "bun:test"
import { mkdir, writeFile, rm } from "fs/promises"
import { join } from "path"
import { tmpdir } from "os"
import {
  ensureDir,
  readEntrypoint,
  writeEntrypoint,
  buildPrompt,
  scan,
  truncateEntrypoint,
} from "@/saeeol/memory"
import { scanMemoryFiles, formatMemoryManifest } from "@/saeeol/memory/scan"
import { memoryAgeDays } from "@/saeeol/memory/age"

const TEST_DIR = join(tmpdir(), `saeeol-memory-test-${Date.now()}`)

async function setupEnv(envDir: string) {
  process.env.SAEEOL_MEMORY_DIR = envDir
  await mkdir(envDir, { recursive: true })
}

async function cleanup() {
  delete process.env.SAEEOL_MEMORY_DIR
  try { await rm(TEST_DIR, { recursive: true }) } catch {}
}

describe("Memory I/O", () => {
  test("ensureDir creates memory directory", async () => {
    const envDir = join(TEST_DIR, "ensure")
    await setupEnv(envDir)
    try {
      const dir = await ensureDir("/fake/project")
      expect(dir).toContain("memory")
      // writing a file should succeed
      await writeFile(join(dir, "test.md"), "content")
    } finally {
      await cleanup()
    }
  })

  test("readEntrypoint returns empty for missing file", async () => {
    const envDir = join(TEST_DIR, "read-missing")
    await setupEnv(envDir)
    try {
      const content = await readEntrypoint("/fake/project")
      expect(content).toBe("")
    } finally {
      await cleanup()
    }
  })

  test("writeEntrypoint then readEntrypoint roundtrip", async () => {
    const envDir = join(TEST_DIR, "roundtrip")
    await setupEnv(envDir)
    try {
      await writeEntrypoint("/fake/project", "# My Memory\n\n- item 1\n- item 2")
      const content = await readEntrypoint("/fake/project")
      expect(content).toContain("My Memory")
      expect(content).toContain("item 1")
    } finally {
      await cleanup()
    }
  })
})

describe("buildPrompt", () => {
  test("generates prompt with empty MEMORY.md", async () => {
    const envDir = join(TEST_DIR, "prompt-empty")
    await setupEnv(envDir)
    try {
      const prompt = await buildPrompt("/fake/project")
      expect(prompt).toContain("# auto memory")
      expect(prompt).toContain("persistent memory system")
      expect(prompt).toContain("MEMORY.md is currently empty")
      expect(prompt).toContain("Types of memory")
      expect(prompt).toContain("user")
      expect(prompt).toContain("feedback")
    } finally {
      await cleanup()
    }
  })

  test("generates prompt with existing MEMORY.md", async () => {
    const envDir = join(TEST_DIR, "prompt-existing")
    await setupEnv(envDir)
    try {
      await writeEntrypoint("/fake/project", "# Notes\n\n- user prefers terse output")
      const prompt = await buildPrompt("/fake/project")
      expect(prompt).toContain("## MEMORY.md")
      expect(prompt).toContain("user prefers terse output")
    } finally {
      await cleanup()
    }
  })
})

describe("scanMemoryFiles", () => {
  test("returns empty for nonexistent directory", async () => {
    const files = await scanMemoryFiles("/nonexistent/path")
    expect(files).toEqual([])
  })

  test("returns empty for empty directory", async () => {
    const envDir = join(TEST_DIR, "scan-empty")
    await setupEnv(envDir)
    try {
      const memDir = await ensureDir("/fake/project")
      const files = await scanMemoryFiles(memDir)
      expect(files).toEqual([])
    } finally {
      await cleanup()
    }
  })

  test("scans .md files with frontmatter", async () => {
    const envDir = join(TEST_DIR, "scan-files")
    await setupEnv(envDir)
    try {
      const memDir = await ensureDir("/fake/project")
      await writeFile(join(memDir, "MEMORY.md"), "# Index")
      await writeFile(join(memDir, "pref.md"), "---\nname: prefs\ndescription: user preferences\ntype: user\n---\n\nPrefers dark mode")
      await writeFile(join(memDir, "bug.md"), "---\nname: bug\ndescription: known bug\ntype: project\n---\n\nAuth fails on edge case")

      const files = await scanMemoryFiles(memDir)
      expect(files.length).toBe(2)
      expect(files.some(f => f.filename === "pref.md")).toBe(true)
      expect(files.some(f => f.filename === "bug.md")).toBe(true)
      // MEMORY.md excluded
      expect(files.every(f => f.filename !== "MEMORY.md")).toBe(true)
      // frontmatter parsed
      const pref = files.find(f => f.filename === "pref.md")
      expect(pref!.description).toBe("user preferences")
      expect(pref!.type).toBe("user")
    } finally {
      await cleanup()
    }
  })

  test("sorts by mtime newest first", async () => {
    const envDir = join(TEST_DIR, "scan-sort")
    await setupEnv(envDir)
    try {
      const memDir = await ensureDir("/fake/project")
      await writeFile(join(memDir, "old.md"), "---\ndescription: old\n---\n")
      // small delay to ensure different mtime
      await new Promise(r => setTimeout(r, 50))
      await writeFile(join(memDir, "new.md"), "---\ndescription: new\n---\n")

      const files = await scanMemoryFiles(memDir)
      expect(files[0].filename).toBe("new.md")
      expect(files[1].filename).toBe("old.md")
    } finally {
      await cleanup()
    }
  })
})

describe("formatMemoryManifest", () => {
  test("formats headers with type tag", () => {
    const manifest = formatMemoryManifest([
      { filename: "a.md", filePath: "/a.md", mtimeMs: 1700000000000, description: "desc a", type: "user" },
    ])
    expect(manifest).toContain("[user]")
    expect(manifest).toContain("desc a")
    expect(manifest).toContain("a.md")
  })

  test("formats headers without description", () => {
    const manifest = formatMemoryManifest([
      { filename: "b.md", filePath: "/b.md", mtimeMs: 1700000000000, description: null, type: undefined },
    ])
    expect(manifest).toContain("b.md")
    // timestamp is present, no extra description after it
    const line = manifest.split("\n")[0]
    expect(line).toMatch(/^- b\.md \(/)
  })
})

describe("truncateEntrypoint edge cases", () => {
  test("handles empty string", () => {
    const result = truncateEntrypoint("")
    expect(result.wasTruncated).toBe(false)
    expect(result.content).toBe("")
  })

  test("handles whitespace-only string", () => {
    const result = truncateEntrypoint("   \n  \n  ")
    expect(result.wasTruncated).toBe(false)
  })

  test("preserves content exactly at limit", () => {
    const exact = "a".repeat(100)
    const result = truncateEntrypoint(exact)
    expect(result.wasTruncated).toBe(false)
    expect(result.content).toBe(exact)
  })
})
