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, NoteIndexRow } from './notes-repository.js';

describe('NotesRepository', () => {
  let tmpRoot: string;
  let db: Database.Database;
  let repo: NotesRepository;

  beforeEach(() => {
    tmpRoot = mkdtempSync(join(tmpdir(), 'notes-repo-test-'));
    db = new Database(join(tmpRoot, 'test.db'));
    runMigrations(db);
    db.prepare(`INSERT INTO users (id, email) VALUES ('alice','alice@x.com'),('bob','bob@x.com')`).run();
    repo = new NotesRepository(db);
  });

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

  describe('upsertNote / getNote / deleteNote', () => {
    it('inserts a new note row', () => {
      const row: NoteIndexRow = {
        owner_id: 'alice', folder: 'cve', file_name: 'foo.md',
        title: 'Foo', visibility: 'public', visibility_scope_org_id: null,
        mode_hint: null, tags_json: '[]', content_size: 100, content_hash: 'h1',
        body: 'body text', updated_at: 1000,
      };
      repo.upsertNote(row);
      const got = repo.getNote('alice', 'cve', 'foo.md');
      expect(got).toMatchObject({ owner_id: 'alice', folder: 'cve', title: 'Foo' });
    });

    it('updates on conflict', () => {
      const row: NoteIndexRow = {
        owner_id: 'alice', folder: 'cve', file_name: 'foo.md',
        title: 'v1', visibility: 'private', visibility_scope_org_id: null,
        mode_hint: null, tags_json: '[]', content_size: 0, content_hash: '',
        body: '', updated_at: 1000,
      };
      repo.upsertNote(row);
      repo.upsertNote({ ...row, title: 'v2', updated_at: 2000 });
      expect(repo.getNote('alice', 'cve', 'foo.md')?.title).toBe('v2');
    });

    it('deletes a note row', () => {
      const row: NoteIndexRow = {
        owner_id: 'alice', folder: 'cve', file_name: 'foo.md',
        title: 'Foo', visibility: 'private', visibility_scope_org_id: null,
        mode_hint: null, tags_json: '[]', content_size: 0, content_hash: '',
        body: '', updated_at: 1000,
      };
      repo.upsertNote(row);
      repo.deleteNote('alice', 'cve', 'foo.md');
      expect(repo.getNote('alice', 'cve', 'foo.md')).toBeNull();
    });
  });

  describe('listFolder', () => {
    it('returns notes in a folder for one owner', () => {
      const base: NoteIndexRow = {
        owner_id: 'alice', folder: 'cve', file_name: 'a.md',
        title: null, visibility: 'private', visibility_scope_org_id: null,
        mode_hint: null, tags_json: '[]', content_size: 0, content_hash: '',
        body: '', updated_at: 1,
      };
      repo.upsertNote({ ...base, file_name: 'a.md' });
      repo.upsertNote({ ...base, file_name: 'b.md' });
      repo.upsertNote({ ...base, folder: 'other', file_name: 'c.md' });
      const list = repo.listFolder('alice', 'cve');
      expect(list.map((r) => r.file_name).sort()).toEqual(['a.md', 'b.md']);
    });
  });

  describe('discover (visibility filtering)', () => {
    function seed() {
      const mk = (over: Partial<NoteIndexRow>): NoteIndexRow => ({
        owner_id: '', folder: '', file_name: '', title: null,
        visibility: 'private', visibility_scope_org_id: null, mode_hint: null,
        tags_json: '[]', content_size: 0, content_hash: '', body: '', updated_at: 1, ...over,
      });
      repo.upsertNote(mk({ owner_id: 'alice', folder: 'cve', file_name: 'pub.md', visibility: 'public' }));
      repo.upsertNote(mk({ owner_id: 'alice', folder: 'cve', file_name: 'org.md', visibility: 'org', visibility_scope_org_id: 'team1' }));
      repo.upsertNote(mk({ owner_id: 'alice', folder: 'cve', file_name: 'priv.md', visibility: 'private' }));
    }

    it('returns public notes to any consumer', () => {
      seed();
      const rows = repo.discover({ user: { id: 'bob', role: 'user', orgIds: [] } as any });
      expect(rows.map((r) => r.file_name).sort()).toEqual(['pub.md']);
    });

    it('returns org notes when consumer is in matching org', () => {
      seed();
      const rows = repo.discover({ user: { id: 'bob', role: 'user', orgIds: ['team1'] } as any });
      expect(rows.map((r) => r.file_name).sort()).toEqual(['org.md', 'pub.md']);
    });

    it('returns all notes to owner regardless of visibility', () => {
      seed();
      const rows = repo.discover({ user: { id: 'alice', role: 'user', orgIds: [] } as any });
      expect(rows.map((r) => r.file_name).sort()).toEqual(['org.md', 'priv.md', 'pub.md']);
    });

    it('filters by owner_id and folder', () => {
      seed();
      const rows = repo.discover({
        user: { id: 'alice', role: 'user', orgIds: [] } as any,
        ownerId: 'alice', folder: 'cve',
      });
      expect(rows).toHaveLength(3);
    });

    it('supports pagination (limit/offset)', () => {
      const base: NoteIndexRow = {
        owner_id: 'alice', folder: 'cve', file_name: '', title: null,
        visibility: 'public', visibility_scope_org_id: null, mode_hint: null,
        tags_json: '[]', content_size: 0, content_hash: '', body: '', updated_at: 1,
      };
      for (let i = 0; i < 10; i++) repo.upsertNote({ ...base, file_name: `f${i}.md` });
      const page1 = repo.discover({ user: { id: 'bob', role: 'user', orgIds: [] } as any, limit: 5, offset: 0 });
      const page2 = repo.discover({ user: { id: 'bob', role: 'user', orgIds: [] } as any, limit: 5, offset: 5 });
      expect(page1).toHaveLength(5);
      expect(page2).toHaveLength(5);
      expect(page1.map((r) => r.file_name)).not.toEqual(page2.map((r) => r.file_name));
    });
  });

  describe('FTS search', () => {
    it('matches title, tags, body via fts MATCH', () => {
      const base: NoteIndexRow = {
        owner_id: 'alice', folder: 'cve', file_name: 'a.md',
        title: 'kubernetes pod crash', visibility: 'public', visibility_scope_org_id: null,
        mode_hint: null, tags_json: '["k8s","crash"]', content_size: 0, content_hash: '',
        body: 'when kubelet restarts unexpectedly', updated_at: 1,
      };
      repo.upsertNote(base);
      const titleHit = repo.search({ user: { id: 'bob', role: 'user', orgIds: [] } as any, query: 'kubernetes' });
      expect(titleHit).toHaveLength(1);
      const tagHit = repo.search({ user: { id: 'bob', role: 'user', orgIds: [] } as any, query: 'k8s' });
      expect(tagHit).toHaveLength(1);
      const bodyHit = repo.search({ user: { id: 'bob', role: 'user', orgIds: [] } as any, query: 'kubelet' });
      expect(bodyHit).toHaveLength(1);
    });

    it('restricts search to subscribed folders when consumerSubscribed=true', () => {
      const base: NoteIndexRow = {
        owner_id: 'alice', folder: 'cve', file_name: 'a.md',
        title: 'kubernetes', visibility: 'public', visibility_scope_org_id: null,
        mode_hint: null, tags_json: '[]', content_size: 0, content_hash: '',
        body: '', updated_at: 1,
      };
      repo.upsertNote(base);
      // bob hasn't subscribed → no results
      const noSub = repo.search({
        user: { id: 'bob', role: 'user', orgIds: [] } as any,
        query: 'kubernetes', consumerSubscribed: true,
      });
      expect(noSub).toHaveLength(0);
      // subscribe bob to alice/cve in search mode
      repo.upsertSubscription({
        consumer_user_id: 'bob', publisher_user_id: 'alice', folder: 'cve',
        mode: 'search', enabled: 1, created_at: 1,
      });
      const withSub = repo.search({
        user: { id: 'bob', role: 'user', orgIds: [] } as any,
        query: 'kubernetes', consumerSubscribed: true,
      });
      expect(withSub).toHaveLength(1);
    });
  });

  describe('subscriptions', () => {
    it('upserts and lists subscriptions', () => {
      repo.upsertSubscription({
        consumer_user_id: 'bob', publisher_user_id: 'alice', folder: 'cve',
        mode: 'search', enabled: 1, created_at: 1,
      });
      const list = repo.listSubscriptions('bob');
      expect(list).toHaveLength(1);
      expect(list[0]).toMatchObject({ publisher_user_id: 'alice', folder: 'cve', mode: 'search' });
    });

    it('deletes a subscription', () => {
      repo.upsertSubscription({
        consumer_user_id: 'bob', publisher_user_id: 'alice', folder: 'cve',
        mode: 'search', enabled: 1, created_at: 1,
      });
      repo.deleteSubscription('bob', 'alice', 'cve');
      expect(repo.listSubscriptions('bob')).toHaveLength(0);
    });

    it('returns inject subscriptions joined with note_index', () => {
      const base: NoteIndexRow = {
        owner_id: 'alice', folder: 'cve', file_name: 'a.md',
        title: 'X', visibility: 'public', visibility_scope_org_id: null,
        mode_hint: null, tags_json: '[]', content_size: 100, content_hash: 'h',
        body: 'b', updated_at: 5,
      };
      repo.upsertNote(base);
      repo.upsertSubscription({
        consumer_user_id: 'bob', publisher_user_id: 'alice', folder: 'cve',
        mode: 'inject', enabled: 1, created_at: 1,
      });
      const rows = repo.injectableForConsumer({ id: 'bob', role: 'user', orgIds: [] } as any);
      expect(rows).toHaveLength(1);
      expect(rows[0]).toMatchObject({ owner_id: 'alice', folder: 'cve', file_name: 'a.md', content_size: 100 });
    });
  });
});
