/**
 * 分流台純函式測試（Arcrun#9 收件夾改裝＝全部待辦分流台，2026-07-08）
 *
 * 驗證兩份 content 契約 → 統一分流項的判定：
 *   1. todo 契約 {"text","marker","owner_tier","project","source","status"} 照 render
 *   2. inbox 契約 {"text","from","status"} 無 tier → 歸 collab 欄＋unclassified 標記
 *   3. 三欄計數只算未完成（status:done 不佔欄）、per-project 清單去重排序
 *   4. 防禦：owner_tier 看不懂／content 非 JSON 不得讓項目蒸發或冒領 ai 欄
 *
 * 樣本形態對照 leo21c live KBDB（epoch 秒 created_at）與 kb-ingest SDD R5/R7 的
 * Logseq marker 詞彙（TODO/DOING/LATER/NOW），非憑空編造。
 */
import { describe, it, expect } from 'vitest';
import { todoToTriageItem, inboxToTriageItem, buildTriageModel, applyTriageCheck } from '../src/lib/console-triage-model';
import type { KbdbEntry } from '../src/lib/console-dashboard-model';

const SEC = (iso: string) => Math.floor(Date.parse(iso) / 1000);

function entry(created: string, content: unknown, type = 'todo'): KbdbEntry {
  return {
    id: crypto.randomUUID(),
    entry_type: type,
    content: typeof content === 'string' ? content : JSON.stringify(content),
    created_at: SEC(created),
  };
}

describe('todoToTriageItem — Logseq todo 契約', () => {
  it('契約全欄照收（text/marker/owner_tier/project/source/status）', () => {
    const it_ = todoToTriageItem(
      entry('2026-07-08T02:00:00Z', {
        text: '把 KBDB 憑證輪替',
        marker: 'TODO',
        owner_tier: 'ai',
        project: 'Arcrun',
        source: 'notes',
        status: 'new',
      }),
    );
    expect(it_.text).toBe('把 KBDB 憑證輪替');
    expect(it_.marker).toBe('TODO');
    expect(it_.tier).toBe('ai');
    expect(it_.unclassified).toBe(false);
    expect(it_.project).toBe('Arcrun');
    expect(it_.source).toBe('notes');
    expect(it_.status).toBe('new');
    expect(it_.origin).toBe('todo');
    expect(it_.at_ms).toBe(Date.parse('2026-07-08T02:00:00Z'));
  });
  it('三欄值 ai/collab/leo 都認；issue 舊詞 mira80/collab15/leo5 當同義收', () => {
    const tier = (v: unknown) => todoToTriageItem(entry('2026-07-08T02:00:00Z', { text: 'x', owner_tier: v }));
    expect(tier('collab').tier).toBe('collab');
    expect(tier('leo').tier).toBe('leo');
    expect(tier('leo').unclassified).toBe(false);
    expect(tier('mira80').tier).toBe('ai');
    expect(tier('collab15').tier).toBe('collab');
    expect(tier('leo5').tier).toBe('leo');
  });
  it('owner_tier 缺席/看不懂 → 歸 collab 欄＋unclassified，不冒領 ai、不蒸發', () => {
    const noTier = todoToTriageItem(entry('2026-07-08T02:00:00Z', { text: '還沒 triage 的', marker: 'LATER' }));
    expect(noTier.tier).toBe('collab');
    expect(noTier.unclassified).toBe(true);
    const weird = todoToTriageItem(entry('2026-07-08T02:00:00Z', { text: 'x', owner_tier: 'boss' }));
    expect(weird.tier).toBe('collab');
    expect(weird.unclassified).toBe(true);
  });
  it('content 非 JSON（不合契約殘資料）→ 原文當 text 誠實顯示，不丟棄', () => {
    const it_ = todoToTriageItem(entry('2026-07-08T02:00:00Z', 'TODO 裸文字殘資料'));
    expect(it_.text).toBe('TODO 裸文字殘資料');
    expect(it_.tier).toBe('collab');
    expect(it_.unclassified).toBe(true);
    expect(it_.status).toBe('new');
  });
  it('status 只有 done 算完成，其餘（含缺席/怪值）一律 new', () => {
    expect(todoToTriageItem(entry('2026-07-08T02:00:00Z', { text: 'x', status: 'done' })).status).toBe('done');
    expect(todoToTriageItem(entry('2026-07-08T02:00:00Z', { text: 'x', status: 'wip' })).status).toBe('new');
    expect(todoToTriageItem(entry('2026-07-08T02:00:00Z', { text: 'x' })).status).toBe('new');
  });
});

describe('inboxToTriageItem — Telegram inbox 契約', () => {
  it('{"text","from","status"} 沿用：無 tier 歸 collab＋unclassified，source 帶 from', () => {
    const it_ = inboxToTriageItem(entry('2026-07-08T03:00:00Z', { text: '幫我查憑證', from: 'leo', status: 'new' }, 'inbox'));
    expect(it_.text).toBe('幫我查憑證');
    expect(it_.tier).toBe('collab');
    expect(it_.unclassified).toBe(true);
    expect(it_.source).toBe('telegram・leo');
    expect(it_.project).toBe('');
    expect(it_.marker).toBe('');
    expect(it_.origin).toBe('inbox');
  });
  it('無 from → source 仍標 telegram；status done 照收', () => {
    const it_ = inboxToTriageItem(entry('2026-07-08T03:00:00Z', { text: 'x', status: 'done' }, 'inbox'));
    expect(it_.source).toBe('telegram');
    expect(it_.status).toBe('done');
  });
});

describe('buildTriageModel — 二源合一 / 三欄計數 / per-project', () => {
  const todo = [
    entry('2026-07-08T02:00:00Z', { text: 'A', owner_tier: 'ai', project: 'Arcrun', status: 'new' }),
    entry('2026-07-08T01:00:00Z', { text: 'B', owner_tier: 'ai', project: 'Arcrun', status: 'done' }),
    entry('2026-07-07T23:00:00Z', { text: 'C', owner_tier: 'leo', project: 'mira', status: 'new' }),
    entry('2026-07-07T22:00:00Z', { text: 'D', project: '', status: 'new' }), // 未分流＋未分項
  ];
  const inbox = [
    entry('2026-07-08T04:00:00Z', { text: 'E', from: 'leo', status: 'new' }, 'inbox'),
    entry('2026-07-07T21:00:00Z', { text: 'F', status: 'done' }, 'inbox'),
  ];

  it('合併排序＝created_at 新→舊，跨兩源交錯', () => {
    const m = buildTriageModel(todo, inbox);
    expect(m.items.map((i) => i.text)).toEqual(['E', 'A', 'B', 'C', 'D', 'F']);
    expect(m.counts.total).toBe(6);
  });
  it('三欄計數只算未完成：done 不佔欄；未分流（todo 無 tier + inbox 全部）歸 collab', () => {
    const m = buildTriageModel(todo, inbox);
    expect(m.counts.ai).toBe(1); // A（B 是 done）
    expect(m.counts.collab).toBe(2); // D（未分流）+ E（inbox）
    expect(m.counts.leo).toBe(1); // C
    expect(m.counts.unclassified).toBe(2); // D + E
    expect(m.counts.done).toBe(2); // B + F
  });
  it('projects 去重、字典序、不含空字串（未分項由 UI 承接）', () => {
    const m = buildTriageModel(todo, inbox);
    expect(m.projects).toEqual(['Arcrun', 'mira']);
  });
  it('空庫誠實回空，不編資料', () => {
    const m = buildTriageModel([], []);
    expect(m.items).toHaveLength(0);
    expect(m.projects).toEqual([]);
    expect(m.counts).toEqual({ ai: 0, collab: 0, leo: 0, unclassified: 0, done: 0, total: 0 });
  });
});

describe('applyTriageCheck — console 勾掉/還原（雙向銷案語意，leo 2026-07-08 拍板）', () => {
  const NOW = '2026-07-08T12:00:00.000Z';

  it('check：status→done + checked_via:console + checked_at，其餘欄位原樣保留', () => {
    const before = JSON.stringify({
      text: '整理 sprint 板',
      marker: 'TODO',
      owner_tier: 'leo',
      project: 'Arcrun',
      source: 'notes',
      status: 'new',
    });
    const after = JSON.parse(applyTriageCheck(before, 'check', NOW));
    expect(after).toEqual({
      text: '整理 sprint 板',
      marker: 'TODO',
      owner_tier: 'leo',
      project: 'Arcrun',
      source: 'notes',
      status: 'done',
      checked_via: 'console',
      checked_at: NOW,
    });
  });

  it('check：inbox 契約 {"text","from","status"} 同樣適用，不掉 from', () => {
    const after = JSON.parse(applyTriageCheck(JSON.stringify({ text: '買咖啡豆', from: 'leo', status: 'new' }), 'check', NOW));
    expect(after.from).toBe('leo');
    expect(after.status).toBe('done');
    expect(after.checked_via).toBe('console');
  });

  it('check：content 非 JSON（不合契約殘資料）→ 包成 {"text":原文}，原文不丟', () => {
    const after = JSON.parse(applyTriageCheck('裸字串待辦', 'check', NOW));
    expect(after).toEqual({ text: '裸字串待辦', status: 'done', checked_via: 'console', checked_at: NOW });
  });

  it('check：已 done 的再勾＝冪等（仍 done，checked_via 蓋成 console）', () => {
    const before = JSON.stringify({ text: 'x', status: 'done', checked_via: 'logseq' });
    const after = JSON.parse(applyTriageCheck(before, 'check', NOW));
    expect(after.status).toBe('done');
    expect(after.checked_via).toBe('console');
  });

  it('restore：status→new、移除 checked_via/checked_at，其餘欄位原樣保留', () => {
    const before = JSON.stringify({
      text: '誤勾的事',
      marker: 'TODO',
      owner_tier: 'ai',
      project: 'mira',
      source: 'notes',
      status: 'done',
      checked_via: 'console',
      checked_at: '2026-07-08T11:00:00.000Z',
    });
    const after = JSON.parse(applyTriageCheck(before, 'restore', NOW));
    expect(after).toEqual({
      text: '誤勾的事',
      marker: 'TODO',
      owner_tier: 'ai',
      project: 'mira',
      source: 'notes',
      status: 'new',
    });
  });

  it('restore：content 非 JSON → 包成 {"text":原文,"status":"new"}', () => {
    const after = JSON.parse(applyTriageCheck('裸字串', 'restore', NOW));
    expect(after).toEqual({ text: '裸字串', status: 'new' });
  });

  it('null/空 content 防禦：不炸，text 誠實為空字串', () => {
    const after = JSON.parse(applyTriageCheck(null, 'check', NOW));
    expect(after.text).toBe('');
    expect(after.status).toBe('done');
  });
});
