// Arcrun#128 — createRecord 的 slot 值可以是「既有 entry 的 id」＝外鍵，不是複製。
//
// 病根（`record-crud.ts` 舊版）：每個 slot 值都無條件 createEntry ⇒ 每設一次外鍵就把被參照的
// 資料複製一份。而 `entry_values` 的約束只有 `UNIQUE(record_id, slot_name)`、`entry_id` 沒有
// 任何 unique ⇒ **儲存層本來就允許共用，壞的只有寫入路徑**。
//
// 測試策略：**真 SQLite**（node:sqlite，同 library-map.test.ts / library-backfill.test.ts 手法）
// 套 migrations/0001_base.sql 原檔——因為本票的驗收標準是「entries 的**列數**變不變」，
// 那是 capture-DB（只驗 SQL 形狀）根本驗不到的東西，必須有真的表在數。
import { describe, it, expect } from 'vitest';
import { DatabaseSync } from 'node:sqlite';
import { readFileSync } from 'node:fs';
import { Hono } from 'hono';
import { recordRoutes } from '../src/routes/records';
import { createRecord, createTemplate, deleteRecord, getRecord, searchByTemplate } from '../src/actions/record-crud';
import { createEntry, getEntry, updateEntry } from '../src/actions/entry-crud';
import type { Bindings } from '../src/types';

/** slot_name -> entry id。 */
type SlotIds = Record<string, string>;

// ── node:sqlite → D1 介面最小 adapter（同 library-map.test.ts 手法）──────────────
function makeSqliteD1(): { db: D1Database; raw: DatabaseSync } {
  const raw = new DatabaseSync(':memory:');
  raw.exec(readFileSync(new URL('../migrations/0001_base.sql', import.meta.url), 'utf8')); // kbdb-sql-ok：測試治具（node:sqlite→D1 shim）套 migration 原檔
  raw.exec('PRAGMA foreign_keys = ON'); // kbdb-sql-ok：測試治具——本地也打開 FK，才測得到「刪掉別人還指著的 entry」會怎樣
  function stmt(sql: string, params: unknown[]) {
    const s = {
      bind(...args: unknown[]) { return stmt(sql, args); },
      async all<T>() { return { results: raw.prepare(sql).all(...(params as never[])) as T[] }; }, // kbdb-sql-ok：測試治具
      async first<T>() { return (raw.prepare(sql).get(...(params as never[])) ?? null) as T | null; }, // kbdb-sql-ok：測試治具
      async run() { raw.prepare(sql).run(...(params as never[])); return { success: true }; }, // kbdb-sql-ok：測試治具
    };
    return s;
  }
  return { db: { prepare: (sql: string) => stmt(sql, []) } as unknown as D1Database, raw };
}

const countEntries = (raw: DatabaseSync): number =>
  (raw.prepare('SELECT COUNT(*) AS n FROM entries').get() as { n: number }).n; // kbdb-sql-ok：測試治具
const countEntryValues = (raw: DatabaseSync): number =>
  (raw.prepare('SELECT COUNT(*) AS n FROM entry_values').get() as { n: number }).n; // kbdb-sql-ok：測試治具
const entryIdOfSlot = (raw: DatabaseSync, recordId: string, slot: string): string | undefined =>
  (raw.prepare('SELECT entry_id FROM entry_values WHERE record_id = ? AND slot_name = ?').get(recordId, slot) as // kbdb-sql-ok：測試治具
    | { entry_id: string }
    | undefined)?.entry_id;

const WIKI_SLOTS = ['title', 'gloss', 'points', 'entities', 'relations'];

/** 模擬「ingest 產出的一則 wiki 卡：五段本來就已經是五筆既有 entry」。 */
async function seedWikiSections(db: D1Database, owner: string | null = 'leo'): Promise<SlotIds> {
  const out: SlotIds = {};
  const sections: Record<string, string> = {
    title: '# KBDB',
    gloss: '## 一句話定義\n三張表的萬用資料層',
    points: '## 要點\n永不加表',
    entities: '## 關鍵實體\nKBDB / template / slot',
    relations: '## 關聯\nKBDB 是 arcrun 的資料層',
  };
  for (const [slot, content] of Object.entries(sections)) {
    const e = await createEntry(db, { content, entry_type: 'block', owner_id: owner });
    out[slot] = e.id;
  }
  return out;
}

describe('Arcrun#128 驗收① — 用既有 entry 的 id 建 record，水池列數不增加', () => {
  it('五個 slot 全部指向既有 entry → entries 總筆數前後相同，只多五筆關聯列', async () => {
    const { db, raw } = makeSqliteD1();
    await createTemplate(db, { name: 'wiki', slots: WIKI_SLOTS, created_by: 'system' });
    const ids = await seedWikiSections(db);

    const entriesBefore = countEntries(raw);
    const evBefore = countEntryValues(raw);

    const rec = await createRecord(db, { template: 'wiki', entry_ids: ids, owner_id: 'leo' });

    const entriesAfter = countEntries(raw);
    const evAfter = countEntryValues(raw);
    console.log(
      `[#128 驗收①] entries: ${entriesBefore} → ${entriesAfter}（差 ${entriesAfter - entriesBefore}）；` +
        `entry_values: ${evBefore} → ${evAfter}（差 ${evAfter - evBefore}）`,
    );

    expect(entriesAfter).toBe(entriesBefore); // 🔴 本票的核心：一筆新 entry 都沒生
    expect(evAfter - evBefore).toBe(5);
    expect(rec.record_id).toMatch(/^rec_/);
  });

  it('對照組（舊路徑）：同樣五個 slot 給字串 → entries 增加五筆（這就是「複製」）', async () => {
    const { db, raw } = makeSqliteD1();
    await createTemplate(db, { name: 'wiki', slots: WIKI_SLOTS, created_by: 'system' });
    await seedWikiSections(db);

    const before = countEntries(raw);
    await createRecord(db, {
      template: 'wiki',
      values: { title: '# KBDB', gloss: 'g', points: 'p', entities: 'e', relations: 'r' },
      owner_id: 'leo',
    });
    const after = countEntries(raw);
    console.log(`[#128 對照組] 舊路徑 entries: ${before} → ${after}（差 ${after - before}）`);
    expect(after - before).toBe(5);
  });
});

describe('Arcrun#128 驗收② — 同一條 entry 被多筆 record 的多個 slot 指到', () => {
  it('一條 entry 同時被兩筆 record、且被同一筆 record 的兩個 slot 指到 → 讀回都正確，水池仍只有一份', async () => {
    const { db, raw } = makeSqliteD1();
    await createTemplate(db, { name: 'wiki', slots: WIKI_SLOTS, created_by: 'system' });
    const shared = await createEntry(db, { content: '共用的那段：KBDB 永不加表', entry_type: 'block', owner_id: 'leo' });
    const other = await createEntry(db, { content: '# 另一張卡', entry_type: 'block', owner_id: 'leo' });

    const before = countEntries(raw);
    const a = await createRecord(db, {
      template: 'wiki',
      // 同一筆 record 的兩個 slot 指同一條 entry（只 UNIQUE(record_id, slot_name)，合法）
      entry_ids: { title: shared.id, gloss: shared.id },
      owner_id: 'leo',
    });
    const b = await createRecord(db, {
      template: 'wiki',
      entry_ids: { title: other.id, points: shared.id },
      owner_id: 'leo',
    });
    const after = countEntries(raw);

    const ra = await getRecord(db, a.record_id);
    const rb = await getRecord(db, b.record_id);
    console.log(
      `[#128 驗收②] entries: ${before} → ${after}；A.title=${ra!.values.title} / A.gloss=${ra!.values.gloss} / B.points=${rb!.values.points}`,
    );

    expect(after).toBe(before);
    expect(ra!.values.title).toBe('共用的那段：KBDB 永不加表');
    expect(ra!.values.gloss).toBe('共用的那段：KBDB 永不加表');
    expect(rb!.values.title).toBe('# 另一張卡');
    expect(rb!.values.points).toBe('共用的那段：KBDB 永不加表');
    // 三個 slot 位置指的都是**同一個** entry id
    expect(entryIdOfSlot(raw, a.record_id, 'title')).toBe(shared.id);
    expect(entryIdOfSlot(raw, a.record_id, 'gloss')).toBe(shared.id);
    expect(entryIdOfSlot(raw, b.record_id, 'points')).toBe(shared.id);
  });
});

describe('Arcrun#128 驗收③ — slot 值就是那筆既有 entry，不是副本', () => {
  it('改那條 entry 的內容 → 兩筆 record 讀回來都是新內容（副本做不到這件事）', async () => {
    const { db } = makeSqliteD1();
    await createTemplate(db, { name: 'wiki', slots: WIKI_SLOTS, created_by: 'system' });
    const shared = await createEntry(db, { content: '第一版', entry_type: 'block', owner_id: 'leo' });
    const a = await createRecord(db, { template: 'wiki', entry_ids: { gloss: shared.id }, owner_id: 'leo' });
    const b = await createRecord(db, { template: 'wiki', entry_ids: { points: shared.id }, owner_id: 'leo' });

    await updateEntry(db, shared.id, { content: '第二版（改在水池那一份）' });

    const ra = await getRecord(db, a.record_id);
    const rb = await getRecord(db, b.record_id);
    console.log(`[#128 驗收③] 改水池後：A.gloss=「${ra!.values.gloss}」／B.points=「${rb!.values.points}」`);
    expect(ra!.values.gloss).toBe('第二版（改在水池那一份）');
    expect(rb!.values.points).toBe('第二版（改在水池那一份）');
  });

  it('createRecord 回傳的 values 帶的是被參照 entry 的現有內容（不是空字串）', async () => {
    const { db } = makeSqliteD1();
    await createTemplate(db, { name: 'wiki', slots: WIKI_SLOTS, created_by: 'system' });
    const e = await createEntry(db, { content: '既有內容', entry_type: 'block', owner_id: 'leo' });
    const rec = await createRecord(db, {
      template: 'wiki',
      values: { title: '新建的' },
      entry_ids: { gloss: e.id },
      owner_id: 'leo',
    });
    expect(rec.values).toEqual({ title: '新建的', gloss: '既有內容' });
  });

  it('kbdb_query（searchByTemplate）撈得到用 entry_ids 建的 record，且 slot 值正確', async () => {
    const { db } = makeSqliteD1();
    await createTemplate(db, { name: 'wiki', slots: WIKI_SLOTS, created_by: 'system' });
    const ids = await seedWikiSections(db, 'leo');
    await createRecord(db, { template: 'wiki', entry_ids: ids, owner_id: 'leo' });

    const recs = await searchByTemplate(db, 'wiki', 'leo');
    console.log(
      `[#128 驗收③] searchByTemplate('wiki','leo') → ${recs.length} 筆，slots=${Object.keys(recs[0]?.values ?? {}).join(',')}`,
    );
    expect(recs).toHaveLength(1);
    expect(recs[0].values.title).toBe('# KBDB');
    expect(recs[0].values.entities).toBe('## 關鍵實體\nKBDB / template / slot');
    expect(recs[0].owner_id).toBe('leo');
  });
});

describe('Arcrun#128 驗收④ — 舊呼叫端行為完全不變', () => {
  it('只給 values：每個 slot 各建一筆新 entry、回傳 values 原樣、template 沒有的 slot 照舊靜默略過', async () => {
    const { db, raw } = makeSqliteD1();
    await createTemplate(db, { id: 'tpl-t', name: 'triplet', slots: ['subject', 'predicate', 'object'], created_by: 'system' });

    const before = countEntries(raw);
    const rec = await createRecord(db, {
      template: 'triplet',
      // `library` 不在 template 的 slots 裡 → 舊行為是「不存，但回傳原樣 echo」
      //（triplet-library-backfill.test.ts 就是靠這個行為在描述存量資料），本次不得改變
      values: { subject: 'A', predicate: 'r', object: 'B', library: 'kb' },
      owner_id: 'leo',
    });
    const after = countEntries(raw);

    expect(after - before).toBe(3);
    expect(rec.values).toEqual({ subject: 'A', predicate: 'r', object: 'B', library: 'kb' });
    expect(rec.template_id).toBe('tpl-t');
    expect(rec.owner_id).toBe('leo');
    const stored = await getRecord(db, rec.record_id);
    expect(stored!.values).toEqual({ subject: 'A', predicate: 'r', object: 'B' });
    // 新建的 entry 沿用舊行為：entry_type='value'、owner_id 帶 record 的歸屬
    const e = await getEntry(db, entryIdOfSlot(raw, rec.record_id, 'subject')!);
    expect(e!.entry_type).toBe('value');
    expect(e!.owner_id).toBe('leo');
  });

  it('POST /records 舊 body（只有 template + values）→ 200，與過去相同', async () => {
    const { db } = makeSqliteD1();
    await createTemplate(db, { name: 'triplet', slots: ['subject', 'predicate', 'object'], created_by: 'system' });
    const app = new Hono<{ Bindings: Bindings }>();
    app.route('/records', recordRoutes);
    const env = { DB: db, ENVIRONMENT: 'test' } as unknown as Bindings;

    const res = await app.request(
      '/records',
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ template: 'triplet', values: { subject: 'A', predicate: 'r', object: 'B' }, owner_id: 'leo' }),
      },
      env,
    );
    expect(res.status).toBe(200);
    const body = (await res.json()) as { success: boolean; record: { values: Record<string, string> } };
    expect(body.success).toBe(true);
    expect(body.record.values).toEqual({ subject: 'A', predicate: 'r', object: 'B' });
  });

  it('POST /records 只給 entry_ids（沒有 values）→ 200（舊版這裡是 400「values required」）', async () => {
    const { db, raw } = makeSqliteD1();
    await createTemplate(db, { name: 'wiki', slots: WIKI_SLOTS, created_by: 'system' });
    const ids = await seedWikiSections(db, 'leo');
    const app = new Hono<{ Bindings: Bindings }>();
    app.route('/records', recordRoutes);
    const env = { DB: db, ENVIRONMENT: 'test' } as unknown as Bindings;

    const before = countEntries(raw);
    const res = await app.request(
      '/records',
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ template: 'wiki', entry_ids: ids, owner_id: 'leo' }),
      },
      env,
    );
    const after = countEntries(raw);
    const body = (await res.json()) as { success: boolean; record: { values: Record<string, string> } };
    console.log(`[#128 route] POST /records（只給 entry_ids）→ ${res.status}；entries ${before} → ${after}`);
    expect(res.status).toBe(200);
    expect(body.record.values.title).toBe('# KBDB');
    expect(after).toBe(before);
  });

  it('POST /records 兩個都沒給 → 400；型別不對 → 400', async () => {
    const { db } = makeSqliteD1();
    await createTemplate(db, { name: 'wiki', slots: WIKI_SLOTS, created_by: 'system' });
    const app = new Hono<{ Bindings: Bindings }>();
    app.route('/records', recordRoutes);
    const env = { DB: db, ENVIRONMENT: 'test' } as unknown as Bindings;
    const post = (body: unknown) =>
      app.request('/records', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }, env);

    expect((await post({ template: 'wiki' })).status).toBe(400);
    expect((await post({ template: 'wiki', entry_ids: { title: 123 } })).status).toBe(400);
    expect((await post({ template: 'wiki', values: ['a'] })).status).toBe(400);
  });
});

describe('Arcrun#128 — 指不到的外鍵要當場講話，而且一列都不寫', () => {
  it('entry id 不存在 → throw entry not found，且 entries／entry_values 都沒動', async () => {
    const { db, raw } = makeSqliteD1();
    await createTemplate(db, { name: 'wiki', slots: WIKI_SLOTS, created_by: 'system' });
    const good = await createEntry(db, { content: 'ok', entry_type: 'block', owner_id: 'leo' });

    const e0 = countEntries(raw);
    const v0 = countEntryValues(raw);
    await expect(
      createRecord(db, { template: 'wiki', entry_ids: { title: good.id, gloss: 'e_不存在' }, owner_id: 'leo' }),
    ).rejects.toThrow(/entry not found: e_不存在/);
    expect(countEntries(raw)).toBe(e0);
    expect(countEntryValues(raw)).toBe(v0); // 檢查全在第一筆 INSERT 之前 ⇒ 沒有半筆殘骸
  });

  it('entry_ids 指到 template 沒有的 slot → throw（不像 values 那樣靜默略過）', async () => {
    const { db } = makeSqliteD1();
    await createTemplate(db, { name: 'wiki', slots: WIKI_SLOTS, created_by: 'system' });
    const e = await createEntry(db, { content: 'x', entry_type: 'block', owner_id: 'leo' });
    await expect(
      createRecord(db, { template: 'wiki', entry_ids: { 沒這個欄位: e.id }, owner_id: 'leo' }),
    ).rejects.toThrow(/slot not in template: 沒這個欄位/);
  });

  it('同一個 slot 同時給 value 與 entry_id → throw（不猜要哪個）', async () => {
    const { db } = makeSqliteD1();
    await createTemplate(db, { name: 'wiki', slots: WIKI_SLOTS, created_by: 'system' });
    const e = await createEntry(db, { content: 'x', entry_type: 'block', owner_id: 'leo' });
    await expect(
      createRecord(db, { template: 'wiki', values: { gloss: '字串' }, entry_ids: { gloss: e.id }, owner_id: 'leo' }),
    ).rejects.toThrow(/slot given both value and entry_id: gloss/);
  });

  it('🔴 租戶邊界：指向別人的 entry → throw owner mismatch，一列都不寫', async () => {
    const { db, raw } = makeSqliteD1();
    await createTemplate(db, { name: 'wiki', slots: WIKI_SLOTS, created_by: 'system' });
    const someoneElse = await createEntry(db, { content: '別人的機密', entry_type: 'block', owner_id: 'alice' });

    const e0 = countEntries(raw);
    const v0 = countEntryValues(raw);
    await expect(
      createRecord(db, { template: 'wiki', entry_ids: { gloss: someoneElse.id }, owner_id: 'leo' }),
    ).rejects.toThrow(/entry owner mismatch/);
    expect(countEntries(raw)).toBe(e0);
    expect(countEntryValues(raw)).toBe(v0);
  });

  it('無主（owner_id=null）的 entry 可以被參照（既有資料多半無主，不能擋死）', async () => {
    const { db } = makeSqliteD1();
    await createTemplate(db, { name: 'wiki', slots: WIKI_SLOTS, created_by: 'system' });
    const orphan = await createEntry(db, { content: '無主資料', entry_type: 'block' });
    const rec = await createRecord(db, { template: 'wiki', entry_ids: { gloss: orphan.id }, owner_id: 'leo' });
    expect(rec.values.gloss).toBe('無主資料');
  });
});

describe('Arcrun#128 配套 — 刪 record 不准刪掉別人還指著的 entry', () => {
  it('共用的 entry 在另一筆 record 刪掉後仍在，且那筆 record 讀得到；沒共用的照舊被刪', async () => {
    const { db, raw } = makeSqliteD1();
    await createTemplate(db, { name: 'wiki', slots: WIKI_SLOTS, created_by: 'system' });
    const shared = await createEntry(db, { content: '兩張卡共用的段落', entry_type: 'block', owner_id: 'leo' });

    const a = await createRecord(db, {
      template: 'wiki',
      values: { title: 'A 專屬' },
      entry_ids: { gloss: shared.id },
      owner_id: 'leo',
    });
    const b = await createRecord(db, { template: 'wiki', entry_ids: { gloss: shared.id }, owner_id: 'leo' });
    const aTitleEntry = entryIdOfSlot(raw, a.record_id, 'title')!;

    const before = countEntries(raw);
    expect(await deleteRecord(db, a.record_id)).toBe(true);
    const after = countEntries(raw);
    console.log(`[#128 配套] 刪掉 A 之後 entries: ${before} → ${after}（只該少掉 A 專屬那一筆）`);

    expect(after).toBe(before - 1); // 只有 A 專屬的 title entry 被刪
    expect(await getEntry(db, aTitleEntry)).toBeNull();
    expect(await getEntry(db, shared.id)).not.toBeNull(); // 🔴 共用那條還在
    const rb = await getRecord(db, b.record_id);
    expect(rb!.values.gloss).toBe('兩張卡共用的段落'); // B 沒被波及
  });

  it('沒有共用時，deleteRecord 行為與舊版相同（底層 entries 全刪）', async () => {
    const { db, raw } = makeSqliteD1();
    await createTemplate(db, { name: 'triplet', slots: ['subject', 'predicate', 'object'], created_by: 'system' });
    const rec = await createRecord(db, {
      template: 'triplet',
      values: { subject: 'A', predicate: 'r', object: 'B' },
      owner_id: 'leo',
    });
    const before = countEntries(raw);
    expect(await deleteRecord(db, rec.record_id)).toBe(true);
    expect(countEntries(raw)).toBe(before - 3);
    expect(countEntryValues(raw)).toBe(0);
    expect(await getRecord(db, rec.record_id)).toBeNull();
  });
});
