import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import Database from 'better-sqlite3';
import { mkdtempSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { runMigrations } from '../db/migrate.js';
import { NotesRepository } from '../notes/notes-repository.js';
import { NotesService } from '../notes/notes-service.js';
import { buildInjectSection, InjectConfig } from './notes-inject.js';

describe('buildInjectSection', () => {
  let tmpRoot: string;
  let db: Database.Database;
  let service: NotesService;
  let bobUser: any;

  beforeEach(() => {
    tmpRoot = mkdtempSync(join(tmpdir(), 'inject-test-'));
    db = new Database(join(tmpRoot, 'test.db'));
    runMigrations(db);
    db.prepare(`INSERT INTO users (id, email, name) VALUES ('alice','a@x.com','Alice'),('bob','b@x.com','Bob')`).run();
    const repo = new NotesRepository(db);
    service = new NotesService({ db, repo, userFolderRoot: tmpRoot, getUserOrgIds: () => ['team1'] });
    bobUser = { id: 'bob', role: 'user', orgIds: [] };
  });

  afterEach(() => { db.close(); rmSync(tmpRoot, { recursive: true, force: true }); });

  it('returns empty string when no inject subscriptions', () => {
    const out = buildInjectSection({ user: bobUser, service, config: { perNoteMaxKb: 8, totalMaxKb: 32, overBudgetStrategy: 'skip_remaining' } });
    expect(out).toBe('');
  });

  it('emits section header and one note when subscribed', () => {
    service.writeNote({ ownerId: 'alice', folder: 'runbooks', fileName: 'failover.md', content: '---\nvisibility: public\n---\nstep 1 do X' });
    service.upsertSubscription({ consumerUser: bobUser, publisherUserId: 'alice', folder: 'runbooks', mode: 'inject', enabled: 1 });
    const out = buildInjectSection({ user: bobUser, service, config: { perNoteMaxKb: 8, totalMaxKb: 32, overBudgetStrategy: 'skip_remaining' } });
    expect(out).toContain('## Subscribed Notes');
    expect(out).toContain('### From Alice/runbooks/failover.md');
    expect(out).toContain('step 1 do X');
  });

  it('skips notes over per_note_max_kb', () => {
    const big = 'x'.repeat(10 * 1024); // 10 KB body
    service.writeNote({ ownerId: 'alice', folder: 'big', fileName: 'huge.md', content: `---\nvisibility: public\n---\n${big}` });
    service.upsertSubscription({ consumerUser: bobUser, publisherUserId: 'alice', folder: 'big', mode: 'inject', enabled: 1 });
    const out = buildInjectSection({ user: bobUser, service, config: { perNoteMaxKb: 4, totalMaxKb: 32, overBudgetStrategy: 'skip_remaining' } });
    expect(out).not.toContain('xxxx');
  });

  it('applies skip_remaining over total budget', () => {
    const med = 'y'.repeat(3 * 1024); // 3 KB
    service.writeNote({ ownerId: 'alice', folder: 'a', fileName: 'one.md', content: `---\nvisibility: public\n---\n${med}` });
    service.writeNote({ ownerId: 'alice', folder: 'a', fileName: 'two.md', content: `---\nvisibility: public\n---\n${med}` });
    service.writeNote({ ownerId: 'alice', folder: 'a', fileName: 'three.md', content: `---\nvisibility: public\n---\n${med}` });
    service.upsertSubscription({ consumerUser: bobUser, publisherUserId: 'alice', folder: 'a', mode: 'inject', enabled: 1 });
    const out = buildInjectSection({ user: bobUser, service, config: { perNoteMaxKb: 8, totalMaxKb: 4, overBudgetStrategy: 'skip_remaining' } });
    // Only first note fits (≈ 3 KB, second would push past 4 KB total)
    const occurrences = (out.match(/### From Alice\//g) || []).length;
    expect(occurrences).toBe(1);
  });

  it('degrade_to_search emits placeholder with skipped note names when over budget', () => {
    const med = 'z'.repeat(3 * 1024); // 3 KB each
    service.writeNote({ ownerId: 'alice', folder: 'kb', fileName: 'first.md', content: `---\nvisibility: public\n---\n${med}` });
    service.writeNote({ ownerId: 'alice', folder: 'kb', fileName: 'second.md', content: `---\nvisibility: public\n---\n${med}` });
    service.upsertSubscription({ consumerUser: bobUser, publisherUserId: 'alice', folder: 'kb', mode: 'inject', enabled: 1 });
    // totalMaxKb=4 means the second note won't fit
    const out = buildInjectSection({
      user: bobUser, service,
      config: { perNoteMaxKb: 8, totalMaxKb: 4, overBudgetStrategy: 'degrade_to_search' },
    });
    expect(out).toContain('## Subscribed Notes');
    // First note fits, second goes into the placeholder
    expect(out).toContain('use SearchNotes');
    // The skipped note should be listed in the placeholder
    expect(out).toContain('second.md');
  });

  it('returns deterministic order on equal updated_at (tiebreak by owner/folder/file)', () => {
    // Insert 2 notes at the same timestamp by direct DB manipulation
    db.prepare(`INSERT INTO note_index (owner_id, folder, file_name, title, visibility, visibility_scope_org_id, mode_hint, tags_json, content_size, content_hash, body, updated_at) VALUES
      ('alice','f','b.md','B','public',NULL,NULL,'[]', 10, 'h', 'body B', 999),
      ('alice','f','a.md','A','public',NULL,NULL,'[]', 10, 'h', 'body A', 999)
    `).run();
    service.upsertSubscription({ consumerUser: bobUser, publisherUserId: 'alice', folder: 'f', mode: 'inject', enabled: 1 });
    const out = buildInjectSection({ user: bobUser, service, config: { perNoteMaxKb: 8, totalMaxKb: 32, overBudgetStrategy: 'skip_remaining' } });
    const aIdx = out.indexOf('### From Alice/f/a.md');
    const bIdx = out.indexOf('### From Alice/f/b.md');
    expect(aIdx).toBeGreaterThan(-1);
    expect(bIdx).toBeGreaterThan(-1);
    expect(aIdx).toBeLessThan(bIdx);
  });
});
