/** LTM unit tests — store, retrieval, embedder cycle */

import { describe, test, expect, beforeEach, afterEach } from "bun:test"
import path from "path"
import { mkdir, rm } from "fs/promises"
import * as Store from "@/ltm/store"
import * as Retrieval from "@/ltm/retrieval"
import type { Memory, LTMConfig } from "@/ltm/types"
import * as Episodic from "@/ltm/memory/episodic"
import * as Procedural from "@/ltm/memory/procedural"

// ── Mock embedder — Ollama 없이 테스트 ──

const mockVectors = new Map<string, number[]>()

/** 간단한 mock 임베딩 (텍스트 해시 기반) */
function mockEmbed(text: string): number[] {
  if (mockVectors.has(text)) return mockVectors.get(text)!
  const vector = Array.from({ length: 384 }, (_, i) => {
    const code = text.charCodeAt(i % text.length) || 0
    return Math.sin(code * (i + 1)) * 0.5 + 0.5
  })
  mockVectors.set(text, vector)
  return vector
}

// 테스트 설정
const testConfig: LTMConfig = {
  enabled: true,
  embeddingModel: "mock",
  vectorStore: "filesystem",
  maxMemories: 100,
  episodic: { enabled: true, summaryInterval: 5, retainDays: 90 },
  semantic: { enabled: true, indexOnFileChange: true },
  procedural: { enabled: true, trackPreferences: true },
  retrieval: { topK: 5, minScore: 0.5, maxTokens: 2000 },
}

// ── Store 테스트 ──

describe("LTM Store", () => {
  test("upsert and search", async () => {
    const vector = mockEmbed("hello world test")
    const memory: Memory = {
      id: "test:1",
      type: "episodic",
      content: "test content about hello world",
      summary: "hello world test",
      vector,
      metadata: {
        source: "test:unit",
        timestamp: Date.now(),
        tags: ["test"],
      },
    }

    await Store.upsert(memory)
    const results = await Store.search(vector, { topK: 5, minScore: 0.0 })
    expect(results.length).toBeGreaterThan(0)
    expect(results[0]!.id).toBe("test:1")
  })

  test("search filters by type", async () => {
    const v1 = mockEmbed("episodic query")
    await Store.upsert({
      id: "test:epi",
      type: "episodic",
      content: "episodic content",
      summary: "episodic query",
      vector: v1,
      metadata: { source: "test", timestamp: Date.now(), tags: [] },
    })

    const v2 = mockEmbed("semantic code query")
    await Store.upsert({
      id: "test:sem",
      type: "semantic",
      content: "code content",
      summary: "semantic code query",
      vector: v2,
      metadata: { source: "test", timestamp: Date.now(), tags: [] },
    })

    const results = await Store.search(v1, { topK: 5, minScore: 0.0, type: "episodic" })
    expect(results.every((m) => m.type === "episodic")).toBe(true)
  })

  test("remove deletes memories", async () => {
    const id = `test-rm-${Date.now()}`
    await Store.upsert({
      id,
      type: "episodic",
      content: "to be removed",
      summary: "remove test",
      vector: mockEmbed("remove test"),
      metadata: { source: "test", timestamp: Date.now(), tags: [] },
    })

    const before = await Store.count()
    await Store.remove([id])
    const after = await Store.count()
    expect(after).toBeLessThanOrEqual(before)
  })

  test("prune removes old memories", async () => {
    const oldTimestamp = Date.now() - 100 * 24 * 60 * 60 * 1000 // 100 days ago
    const id = `test-old-${Date.now()}`
    await Store.upsert({
      id,
      type: "episodic",
      content: "old memory",
      summary: "old memory content",
      vector: mockEmbed("old memory content"),
      metadata: { source: "test", timestamp: oldTimestamp, tags: [] },
    })

    const pruned = await Store.prune(90 * 24 * 60 * 60 * 1000) // 90 days
    expect(pruned).toBeGreaterThanOrEqual(0)
  })

  test("upsert updates existing memory", async () => {
    const id = "test:update"
    const v1 = mockEmbed("version 1")
    await Store.upsert({
      id,
      type: "semantic",
      content: "version 1",
      summary: "version 1",
      vector: v1,
      metadata: { source: "test", timestamp: 1, tags: [] },
    })

    const v2 = mockEmbed("version 2 updated")
    await Store.upsert({
      id,
      type: "semantic",
      content: "version 2",
      summary: "version 2 updated",
      vector: v2,
      metadata: { source: "test", timestamp: 2, tags: [] },
    })

    const all = await Store.list()
    const found = all.find((m) => m.id === id)
    expect(found).toBeDefined()
    expect(found!.content).toBe("version 2")
  })

  test("count returns total memories", async () => {
    const count = await Store.count()
    expect(typeof count).toBe("number")
    expect(count).toBeGreaterThanOrEqual(0)
  })
})

// ── Episodic Memory 테스트 ──

describe("LTM Episodic Memory", () => {
  test("shouldSummarize triggers at interval", () => {
    expect(Episodic.shouldSummarize(10, 10)).toBe(true)
    expect(Episodic.shouldSummarize(5, 10)).toBe(false)
    expect(Episodic.shouldSummarize(20, 10)).toBe(true)
    expect(Episodic.shouldSummarize(0, 10)).toBe(false)
  })
})

// ── Procedural Memory 테스트 ──

describe("LTM Procedural Memory", () => {
  test("extractStyleSignals detects indentation", () => {
    const signals = Procedural.extractStyleSignals("test.ts", "const x = 1\n  const y = 2\n  const z = 3")
    const indent = signals.find((s) => s.pattern === "indent")
    expect(indent).toBeDefined()
    expect(indent!.evidence).toContain("2-space")
  })

  test("extractStyleSignals detects Korean comments", () => {
    const signals = Procedural.extractStyleSignals("test.ts", "// 한국어 주석\nconst x = 1")
    const commentLang = signals.find((s) => s.pattern === "comment-language")
    expect(commentLang).toBeDefined()
    expect(commentLang!.evidence).toContain("Korean")
  })

  test("extractStyleSignals detects quote style", () => {
    const signals = Procedural.extractStyleSignals("test.ts", "const a = 'hello'\nconst b = 'world'\nconst c = 'test'")
    const quotes = signals.find((s) => s.pattern === "quotes")
    expect(quotes).toBeDefined()
  })

  test("extractStyleSignals returns empty for empty file", () => {
    const signals = Procedural.extractStyleSignals("test.txt", "")
    expect(signals).toBeArray()
  })
})

// ── Retrieval 포맷 테스트 ──

describe("LTM Retrieval", () => {
  test("format produces context block", () => {
    const memories: Memory[] = [
      {
        id: "test:r1",
        type: "episodic",
        content: "auth bug fix",
        summary: "fixed CSRF vulnerability in auth/login.ts",
        vector: [],
        metadata: { source: "session:abc", timestamp: Date.now(), tags: ["fix", "auth"] },
      },
      {
        id: "test:r2",
        type: "semantic",
        content: "express middleware",
        summary: "project uses Express middleware chain pattern",
        vector: [],
        metadata: { source: "file:src/app.ts", timestamp: Date.now(), tags: ["architecture"] },
      },
    ]

    const formatted = Retrieval.format(memories)
    expect(formatted).toContain("[long-term memory")
    expect(formatted).toContain("episodic")
    expect(formatted).toContain("semantic")
    expect(formatted).toContain("CSRF")
  })

  test("format returns empty for no memories", () => {
    const formatted = Retrieval.format([])
    expect(formatted).toBe("")
  })
})
