import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import express from 'express';
import request from 'supertest';
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 { createNotesApi } from './notes-api.js';

describe('notes-api', () => {
  let tmpRoot: string;
  let db: Database.Database;
  let service: NotesService;
  let auditCalls: any[];

  function makeApp(userId: string, orgIds: string[] = []): express.Application {
    const app = express();
    app.use((req, _res, next) => {
      (req as any).user = { id: userId, role: 'user', orgIds };
      next();
    });
    app.use('/api/notes', createNotesApi({ service }));
    return app;
  }

  beforeEach(() => {
    tmpRoot = mkdtempSync(join(tmpdir(), 'notes-api-test-'));
    db = new Database(join(tmpRoot, 'test.db'));
    runMigrations(db);
    db.prepare(`INSERT INTO users (id, email, name) VALUES ('alice','alice@x.com','Alice'),('bob','bob@x.com','Bob')`).run();
    const repo = new NotesRepository(db);
    auditCalls = [];
    service = new NotesService({
      db, repo, userFolderRoot: tmpRoot,
      getUserOrgIds: () => ['team1'],
      audit: (action, actor, target) => auditCalls.push({ action, actor, target }),
    });
  });

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

  describe('GET /discover', () => {
    it('returns visible notes for the consumer', async () => {
      service.writeNote({ ownerId: 'alice', folder: 'cve', fileName: 'pub.md', content: '---\nvisibility: public\n---\nbody' });
      service.writeNote({ ownerId: 'alice', folder: 'cve', fileName: 'priv.md', content: '---\nvisibility: private\n---\nbody' });
      const res = await request(makeApp('bob')).get('/api/notes/discover');
      expect(res.status).toBe(200);
      expect(res.body.rows.map((r: any) => r.file_name)).toEqual(['pub.md']);
    });

    it('supports keyword search via q', async () => {
      service.writeNote({ ownerId: 'alice', folder: 'cve', fileName: 'pub.md', content: '---\ntitle: kubernetes\nvisibility: public\n---\nbody' });
      const res = await request(makeApp('bob')).get('/api/notes/discover?q=kubernetes');
      expect(res.status).toBe(200);
      expect(res.body.rows).toHaveLength(1);
    });

    it('paginates', async () => {
      for (let i = 0; i < 12; i++) {
        service.writeNote({ ownerId: 'alice', folder: 'cve', fileName: `f${i}.md`, content: `---\nvisibility: public\n---\nbody ${i}` });
      }
      const res = await request(makeApp('bob')).get('/api/notes/discover?limit=5&offset=5');
      expect(res.status).toBe(200);
      expect(res.body.rows).toHaveLength(5);
    });

    it('returns 401 if unauthenticated (authActive=true, default)', async () => {
      const app = express();
      app.use('/api/notes', createNotesApi({ service }));
      const res = await request(app).get('/api/notes/discover');
      expect(res.status).toBe(401);
    });

    it('synthesizes local user when authActive=false (no-auth deployment)', async () => {
      // Regression for issue #347: /api/notes/discover returned 401 in
      // no-auth deployments because the route had no synthetic-local
      // fallback that user-folder-api / dashboard-api already had,
      // breaking the Notes panel under User Folder.
      db.prepare(`INSERT INTO users (id, email, name) VALUES ('local','local@x.com','Local')`).run();
      service.writeNote({ ownerId: 'local', folder: 'cve', fileName: 'mine.md', content: '---\nvisibility: private\n---\nlocal note' });
      const app = express();
      app.use('/api/notes', createNotesApi({ service, authActive: false }));
      const res = await request(app).get('/api/notes/discover?owner_id=me');
      expect(res.status, JSON.stringify(res.body)).toBe(200);
      const names = res.body.rows.map((r: any) => r.file_name);
      expect(names).toContain('mine.md');
    });

    it('resolves owner_id=me to the caller user.id', async () => {
      // Alice writes notes; the FileTree under the Notes tab queries
      // /discover?owner_id=me — must resolve to alice, not the literal string "me".
      service.writeNote({ ownerId: 'alice', folder: 'cve', fileName: 'a.md', content: '---\nvisibility: private\n---\nmine' });
      service.writeNote({ ownerId: 'alice', folder: 'cve', fileName: 'b.md', content: '---\nvisibility: public\n---\nmine2' });
      service.writeNote({ ownerId: 'bob',   folder: 'cve', fileName: 'c.md', content: '---\nvisibility: public\n---\nbob' });
      const res = await request(makeApp('alice')).get('/api/notes/discover?owner_id=me');
      expect(res.status).toBe(200);
      const names = res.body.rows.map((r: any) => r.file_name).sort();
      expect(names).toEqual(['a.md', 'b.md']);
    });
  });

  describe('GET /file', () => {
    it('returns note body for visible note and logs audit', async () => {
      service.writeNote({ ownerId: 'alice', folder: 'cve', fileName: 'pub.md', content: '---\nvisibility: public\n---\nbody content' });
      const res = await request(makeApp('bob')).get('/api/notes/file?owner_id=alice&folder=cve&file_name=pub.md');
      expect(res.status).toBe(200);
      expect(res.body.body).toContain('body content');
      expect(auditCalls).toHaveLength(1);
      expect(auditCalls[0]).toMatchObject({ action: 'read_note', actor: 'bob' });
    });

    it('returns 404 for private note', async () => {
      service.writeNote({ ownerId: 'alice', folder: 'cve', fileName: 'priv.md', content: '---\nvisibility: private\n---\nbody' });
      const res = await request(makeApp('bob')).get('/api/notes/file?owner_id=alice&folder=cve&file_name=priv.md');
      expect(res.status).toBe(404);
      expect(auditCalls).toHaveLength(0);
    });

    it('rejects invalid folder/file_name', async () => {
      const res = await request(makeApp('bob')).get('/api/notes/file?owner_id=alice&folder=../etc&file_name=passwd');
      expect(res.status).toBe(400);
    });
  });

  describe('reindex and inject-preview', () => {
    it('POST /reindex rebuilds the index for the requester', async () => {
      const fs = require('fs');
      fs.mkdirSync(join(tmpRoot, 'alice', 'notes', 'cve'), { recursive: true });
      fs.writeFileSync(join(tmpRoot, 'alice', 'notes', 'cve', 'foo.md'), '---\nvisibility: public\n---\nbody');
      const res = await request(makeApp('alice')).post('/api/notes/reindex?owner_id=me');
      expect(res.status).toBe(200);
      expect(res.body.indexed).toBe(1);
    });

    it('GET /inject-preview shows what will be injected', async () => {
      service.writeNote({ ownerId: 'alice', folder: 'cve', fileName: 'foo.md', content: '---\nvisibility: public\n---\nbody A' });
      service.writeNote({ ownerId: 'alice', folder: 'cve', fileName: 'bar.md', content: '---\nvisibility: public\n---\nbody B' });
      await request(makeApp('bob'))
        .put('/api/notes/subscriptions')
        .send({ publisher_user_id: 'alice', folder: 'cve', mode: 'inject', enabled: true });
      const res = await request(makeApp('bob')).get('/api/notes/inject-preview');
      expect(res.status).toBe(200);
      expect(res.body.items).toHaveLength(2);
      expect(res.body.total_kb).toBeGreaterThan(0);
      expect(res.body.budget_kb).toBeDefined();
    });
  });

  describe('subscriptions', () => {
    it('GET /subscriptions returns the user\'s subscriptions', async () => {
      service.writeNote({ ownerId: 'alice', folder: 'cve', fileName: 'foo.md', content: '---\nvisibility: public\n---\nbody' });
      // Create a subscription via PUT
      await request(makeApp('bob'))
        .put('/api/notes/subscriptions')
        .send({ publisher_user_id: 'alice', folder: 'cve', mode: 'search', enabled: true });
      const res = await request(makeApp('bob')).get('/api/notes/subscriptions');
      expect(res.status).toBe(200);
      expect(res.body.rows).toHaveLength(1);
      expect(res.body.rows[0]).toMatchObject({ publisher_user_id: 'alice', folder: 'cve', mode: 'search' });
    });

    it('PUT /subscriptions creates a subscription if folder is visible', async () => {
      service.writeNote({ ownerId: 'alice', folder: 'cve', fileName: 'foo.md', content: '---\nvisibility: public\n---\nbody' });
      const res = await request(makeApp('bob'))
        .put('/api/notes/subscriptions')
        .send({ publisher_user_id: 'alice', folder: 'cve', mode: 'inject', enabled: true });
      expect(res.status).toBe(200);
    });

    it('PUT /subscriptions rejects if folder has no visible notes', async () => {
      service.writeNote({ ownerId: 'alice', folder: 'cve', fileName: 'priv.md', content: '---\nvisibility: private\n---\nbody' });
      const res = await request(makeApp('bob'))
        .put('/api/notes/subscriptions')
        .send({ publisher_user_id: 'alice', folder: 'cve', mode: 'search', enabled: true });
      expect(res.status).toBe(403);
    });

    it('PUT /subscriptions rejects invalid mode', async () => {
      const res = await request(makeApp('bob'))
        .put('/api/notes/subscriptions')
        .send({ publisher_user_id: 'alice', folder: 'cve', mode: 'bogus', enabled: true });
      expect(res.status).toBe(400);
    });

    it('DELETE /subscriptions removes a subscription', async () => {
      service.writeNote({ ownerId: 'alice', folder: 'cve', fileName: 'pub.md', content: '---\nvisibility: public\n---\nbody' });
      await request(makeApp('bob'))
        .put('/api/notes/subscriptions')
        .send({ publisher_user_id: 'alice', folder: 'cve', mode: 'search', enabled: true });
      const del = await request(makeApp('bob'))
        .delete('/api/notes/subscriptions?publisher_user_id=alice&folder=cve');
      expect(del.status).toBe(200);
      const list = await request(makeApp('bob')).get('/api/notes/subscriptions');
      expect(list.body.rows).toHaveLength(0);
    });
  });
});
