import { describe, it, expect } from 'vitest';
import Database from 'better-sqlite3';
import { runMigrations, isFts5Available } from './migrate.js';
import { Repository } from './repository.js';

describe('task_comment_index schema', () => {
  it('creates the index table and FTS5 stays in sync via triggers', () => {
    const db = new Database(':memory:');
    // 依存テーブルの最小定義（local_tasks / local_task_comments は schema.sql に依存するため、
    // ここでは索引テーブルだけを検証する最小 DB を作る）
    runMigrations(db); // 索引テーブル＋FTS＋トリガを作る（他 migrate は table 不在で no-op）
    expect(isFts5Available(db)).toBe(true);

    db.prepare(
      "INSERT INTO task_comment_index (comment_id, task_id, author, kind, created_at, text) VALUES (?,?,?,?,?,?)",
    ).run(1, 100, 'user', 'request', '2026-07-02', '請求書の認証エラーを調べて');

    const hit = db
      .prepare("SELECT rowid FROM task_comment_fts WHERE task_comment_fts MATCH ?")
      .get('"認証エラー"') as { rowid: number } | undefined;
    expect(hit?.rowid).toBe(1);

    db.prepare('DELETE FROM task_comment_index WHERE comment_id = ?').run(1);
    const afterDelete = db
      .prepare("SELECT rowid FROM task_comment_fts WHERE task_comment_fts MATCH ?")
      .get('"認証エラー"');
    expect(afterDelete).toBeUndefined();
    db.close();
  });

  it('a bare Repository construction has the FTS table (initSchema creates it)', () => {
    const repo = new Repository(':memory:');
    const db = (repo as any).db;
    const hasFts = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='task_comment_fts'").get();
    expect(hasFts).toBeTruthy();
  });

  it('indexes a comment on addLocalTaskComment and removes it on deleteLocalTask', async () => {
    const repo = new Repository(':memory:');
    const task = await repo.createLocalTask({ title: 'T', body: 'p', ownerId: 'u1' } as any);
    await repo.addLocalTaskComment(task.id, 'user', '認証エラーの調査', 'request');

    const rows = (repo as any).db
      .prepare("SELECT rowid FROM task_comment_fts WHERE task_comment_fts MATCH ?")
      .all('"認証エラー"');
    expect(rows.length).toBe(1);

    await repo.deleteLocalTask(task.id);
    const after = (repo as any).db
      .prepare("SELECT rowid FROM task_comment_fts WHERE task_comment_fts MATCH ?")
      .all('"認証エラー"');
    expect(after.length).toBe(0);
  });

  it('backfills existing comments idempotently', () => {
    const db = new Database(':memory:');
    // 依存テーブルを最小構築
    db.exec(`CREATE TABLE local_task_comments (id INTEGER PRIMARY KEY, task_id INTEGER, author TEXT, kind TEXT, body TEXT, attachments TEXT, created_at TEXT DEFAULT '2026-07-02');`);
    runMigrations(db); // 索引テーブル作成（backfill は空なので no-op）
    db.prepare("INSERT INTO local_task_comments (task_id, author, kind, body) VALUES (1,'user','request','認証エラー調査')").run();

    runMigrations(db); // 2回目: backfill が既存コメントを拾う
    const first = db.prepare('SELECT COUNT(*) c FROM task_comment_index').get() as { c: number };
    runMigrations(db); // 3回目: 冪等（重複しない）
    const second = db.prepare('SELECT COUNT(*) c FROM task_comment_index').get() as { c: number };
    expect(first.c).toBe(1);
    expect(second.c).toBe(1);
    db.close();
  });
});

describe('Repository.searchWorkspaceTasks', () => {
  it('finds sibling task comments in the same space, excludes self and other spaces', async () => {
    const repo = new Repository(':memory:');
    // メンバーシップ確認は spaces.owner_id を見るため、space-1 を owner='u1' で先に作る。
    const space1 = await repo.createSpace({ kind: 'case', title: 'Space 1', ownerId: 'u1' });
    const a = await repo.createLocalTask({ title: 'A', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    const b = await repo.createLocalTask({ title: 'B', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    const c = await repo.createLocalTask({ title: 'C', body: 'p', ownerId: 'u1', spaceId: 'space-2' } as any);
    await repo.addLocalTaskComment(a.id, 'agent', '認証エラーは再ログインで解消', 'result');
    await repo.addLocalTaskComment(b.id, 'agent', '認証エラーの別事例', 'result');
    await repo.addLocalTaskComment(c.id, 'agent', '認証エラー（別スペース）', 'result');

    const hits = await repo.searchWorkspaceTasks(a.id, 'u1', '認証エラー');
    const ids = hits.map((h) => h.taskId);
    expect(ids).toContain(b.id);      // 同スペースの他タスク
    expect(ids).not.toContain(a.id);  // 自タスク除外
    expect(ids).not.toContain(c.id);  // 別スペース除外
  });

  it('multi-word query is implicit AND (all terms must be present, order-independent)', async () => {
    const repo = new Repository(':memory:');
    const space1 = await repo.createSpace({ kind: 'case', title: 'Space 1', ownerId: 'u1' });
    const a = await repo.createLocalTask({ title: 'A', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    const both = await repo.createLocalTask({ title: 'Both', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    const oneOnly = await repo.createLocalTask({ title: 'OneOnly', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    const reordered = await repo.createLocalTask({ title: 'Reordered', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    // 全語 (3文字以上) を含む → FTS AND 経路
    await repo.addLocalTaskComment(both.id, 'agent', 'ログイン画面の認証まわりを修正しました', 'result');
    await repo.addLocalTaskComment(oneOnly.id, 'agent', 'ログイン画面のレイアウトだけ調整', 'result'); // 「認証」を含まない
    await repo.addLocalTaskComment(reordered.id, 'agent', '認証の話。あとでログインの導線も見る', 'result'); // 語順が逆でも両方含む

    const hits = await repo.searchWorkspaceTasks(a.id, 'u1', 'ログイン 認証');
    const ids = hits.map((h) => h.taskId);
    expect(ids).toContain(both.id);         // 両語を含む
    expect(ids).toContain(reordered.id);    // 語順が違っても両語を含めばヒット (AND, フレーズではない)
    expect(ids).not.toContain(oneOnly.id);  // 片方しか含まない → AND で除外
  });

  it('implicit AND also works for short (<3 char) terms via the LIKE path', async () => {
    const repo = new Repository(':memory:');
    const space1 = await repo.createSpace({ kind: 'case', title: 'Space 1', ownerId: 'u1' });
    const a = await repo.createLocalTask({ title: 'A', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    const both = await repo.createLocalTask({ title: 'Both', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    const oneOnly = await repo.createLocalTask({ title: 'OneOnly', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    await repo.addLocalTaskComment(both.id, 'agent', 'PR の CI が赤い', 'result');   // 「CI」(2)と「PR」(2)両方
    await repo.addLocalTaskComment(oneOnly.id, 'agent', 'CI は緑になった', 'result'); // 「CI」だけ

    // 「PR」「CI」はどちらも2文字 → LIKE 経路の AND
    const hits = await repo.searchWorkspaceTasks(a.id, 'u1', 'PR CI');
    const ids = hits.map((h) => h.taskId);
    expect(ids).toContain(both.id);
    expect(ids).not.toContain(oneOnly.id); // 「PR」を含まない
  });

  it('drops quote-only "words" so they do not poison the AND (returns real matches, no throw)', async () => {
    const repo = new Repository(':memory:');
    const space1 = await repo.createSpace({ kind: 'case', title: 'Space 1', ownerId: 'u1' });
    const a = await repo.createLocalTask({ title: 'A', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    const b = await repo.createLocalTask({ title: 'B', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    await repo.addLocalTaskComment(b.id, 'agent', '認証まわりを直した', 'result');

    // 先頭が引用符だけの「語」。除去後は「認証まわり」だけが残り、ヒットする。
    const mixed = await repo.searchWorkspaceTasks(a.id, 'u1', '"""" 認証まわり');
    expect(mixed.map((h) => h.taskId)).toContain(b.id);

    // 引用符のみ = 実質的な検索語なし → 例外を投げず空配列。
    await expect(repo.searchWorkspaceTasks(a.id, 'u1', '""""')).resolves.toEqual([]);
  });

  it('handles short queries via LIKE and sanitizes FTS operators', async () => {
    const repo = new Repository(':memory:');
    const space1 = await repo.createSpace({ kind: 'case', title: 'Space 1', ownerId: 'u1' });
    const a = await repo.createLocalTask({ title: 'A', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    const b = await repo.createLocalTask({ title: 'B', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    await repo.addLocalTaskComment(b.id, 'agent', 'AND OR "quoted" 認証', 'result');
    await expect(repo.searchWorkspaceTasks(a.id, 'u1', 'AND OR "quoted"')).resolves.toBeInstanceOf(Array); // 例外を投げない
    const short = await repo.searchWorkspaceTasks(a.id, 'u1', '証'); // 1文字 → LIKE
    expect(short.map((h) => h.taskId)).toContain(b.id);
  });

  it('confused-deputy: a non-member of the space gets no results even for a searchable term', async () => {
    const repo = new Repository(':memory:');
    // space owned by u1; u2 is neither the owner nor a space_members row.
    const space1 = await repo.createSpace({ kind: 'case', title: 'Space 1', ownerId: 'u1' });
    const a = await repo.createLocalTask({ title: 'A', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    const b = await repo.createLocalTask({ title: 'B', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    await repo.addLocalTaskComment(b.id, 'agent', '機密情報の認証エラー詳細', 'result');

    // currentTaskId(a) belongs to the space (non-null space_id), so the membership
    // guard actually runs; ownerId 'u2' fails it → must return [] regardless of match.
    const hits = await repo.searchWorkspaceTasks(a.id, 'u2', '認証エラー');
    expect(hits).toEqual([]);
  });

  it('reads neighbors around a comment ref within the same space', async () => {
    const repo = new Repository(':memory:');
    const a = await repo.createLocalTask({ title: 'A', body: 'p', ownerId: 'u1', spaceId: 's1' } as any);
    const b = await repo.createLocalTask({ title: 'B', body: 'p', ownerId: 'u1', spaceId: 's1' } as any);
    const c1 = await repo.addLocalTaskComment(b.id, 'user', '最初の依頼', 'request');
    const c2 = await repo.addLocalTaskComment(b.id, 'agent', '認証エラー発生', 'result');
    const c3 = await repo.addLocalTaskComment(b.id, 'agent', '解消した', 'result');
    const around = await repo.readWorkspaceTaskAround(a.id, 'u1', c2.id, 1, 1);
    expect(around!.taskId).toBe(b.id);
    expect(around!.comments.map((x) => x.commentId)).toEqual([c1.id, c2.id, c3.id]);
    expect(around!.comments.find((x) => x.isCenter)!.commentId).toBe(c2.id);
  });

  it('personal-space branch (space_id NULL): finds same-owner tasks only, isolates other owners', async () => {
    const repo = new Repository(':memory:');
    // No spaceId → space_id IS NULL. Scope falls back to owner match.
    const a = await repo.createLocalTask({ title: 'A', body: 'p', ownerId: 'u1' } as any);
    const b = await repo.createLocalTask({ title: 'B', body: 'p', ownerId: 'u1' } as any);
    const other = await repo.createLocalTask({ title: 'Other', body: 'p', ownerId: 'u2' } as any);
    await repo.addLocalTaskComment(b.id, 'agent', '個人スペースの認証エラー', 'result');
    await repo.addLocalTaskComment(other.id, 'agent', '別ユーザーの認証エラー', 'result');

    const hits = await repo.searchWorkspaceTasks(a.id, 'u1', '認証エラー');
    const ids = hits.map((h) => h.taskId);
    expect(ids).toContain(b.id);          // 同じ owner の個人タスク
    expect(ids).not.toContain(a.id);      // 自タスク除外
    expect(ids).not.toContain(other.id);  // 別 owner の個人タスクは越境しない
  });

  it('filters by kind, author, and task_id (each ANDed within the space scope)', async () => {
    const repo = new Repository(':memory:');
    const space1 = await repo.createSpace({ kind: 'case', title: 'Space 1', ownerId: 'u1' });
    const a = await repo.createLocalTask({ title: 'A', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    const b = await repo.createLocalTask({ title: 'B', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    const d = await repo.createLocalTask({ title: 'D', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    await repo.addLocalTaskComment(b.id, 'user', '認証エラーを調べて', 'request');
    await repo.addLocalTaskComment(b.id, 'agent', '認証エラーは解消した', 'result');
    await repo.addLocalTaskComment(d.id, 'agent', '別タスクの認証エラー', 'result');

    // kind filter: only the 'result' comment in b + d's result (both agent/result)
    const byKind = await repo.searchWorkspaceTasks(a.id, 'u1', '認証エラー', { kind: 'result' });
    expect(byKind.every((h) => h.kind === 'result')).toBe(true);
    expect(byKind.some((h) => h.kind === 'request')).toBe(false);

    // author filter: only 'user' authored comments
    const byAuthor = await repo.searchWorkspaceTasks(a.id, 'u1', '認証エラー', { author: 'user' });
    expect(byAuthor.length).toBeGreaterThan(0);
    expect(byAuthor.every((h) => h.author === 'user')).toBe(true);

    // task_id filter: pin to b only
    const byTask = await repo.searchWorkspaceTasks(a.id, 'u1', '認証エラー', { taskId: b.id });
    expect(byTask.length).toBeGreaterThan(0);
    expect(byTask.every((h) => h.taskId === b.id)).toBe(true);
  });

  it('task_id filter cannot escape the space scope (foreign-space task id yields nothing)', async () => {
    const repo = new Repository(':memory:');
    const space1 = await repo.createSpace({ kind: 'case', title: 'Space 1', ownerId: 'u1' });
    const a = await repo.createLocalTask({ title: 'A', body: 'p', ownerId: 'u1', spaceId: space1.id } as any);
    const foreign = await repo.createLocalTask({ title: 'Foreign', body: 'p', ownerId: 'u1', spaceId: 'space-2' } as any);
    await repo.addLocalTaskComment(foreign.id, 'agent', '別スペースの認証エラー', 'result');

    // Pinning task_id to a task in ANOTHER space must not surface it — the scope
    // clause is ANDed with the task_id filter.
    const hits = await repo.searchWorkspaceTasks(a.id, 'u1', '認証エラー', { taskId: foreign.id });
    expect(hits).toEqual([]);
  });
});

describe('Repository.readWorkspaceTaskAround (scope rejection)', () => {
  it('returns null for a comment whose task is in a different space', async () => {
    const repo = new Repository(':memory:');
    const a = await repo.createLocalTask({ title: 'A', body: 'p', ownerId: 'u1', spaceId: 's1' } as any);
    const c = await repo.createLocalTask({ title: 'C', body: 'p', ownerId: 'u1', spaceId: 's2' } as any);
    const cc = await repo.addLocalTaskComment(c.id, 'agent', '別スペースの本文', 'result');
    // current task a is in s1; target comment lives in s2 → out of scope → null
    const around = await repo.readWorkspaceTaskAround(a.id, 'u1', cc.id, 1, 1);
    expect(around).toBeNull();
  });

  it('returns null for a personal-space comment owned by a different user', async () => {
    const repo = new Repository(':memory:');
    const a = await repo.createLocalTask({ title: 'A', body: 'p', ownerId: 'u1' } as any);        // space_id NULL
    const other = await repo.createLocalTask({ title: 'O', body: 'p', ownerId: 'u2' } as any);     // space_id NULL, other owner
    const oc = await repo.addLocalTaskComment(other.id, 'agent', '別ユーザーの本文', 'result');
    const around = await repo.readWorkspaceTaskAround(a.id, 'u1', oc.id, 1, 1);
    expect(around).toBeNull();
  });

  it('returns null for a nonexistent comment ref and a nonexistent current task', async () => {
    const repo = new Repository(':memory:');
    const a = await repo.createLocalTask({ title: 'A', body: 'p', ownerId: 'u1', spaceId: 's1' } as any);
    expect(await repo.readWorkspaceTaskAround(a.id, 'u1', 999999, 1, 1)).toBeNull(); // missing comment
    expect(await repo.readWorkspaceTaskAround(888888, 'u1', 1, 1, 1)).toBeNull();     // missing current task
  });
});
