/**
 * memory-api.test.ts — unit tests for /api/local/memory router
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import express from 'express';
import request from 'supertest';
import { mkdtempSync, rmSync, mkdirSync, existsSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { createMemoryApi } from './memory-api.js';
import { upsertMemoryEntry } from '../user-folder/memory.js';

// ── Helpers ────────────────────────────────────────────────────────────────────

const USER_A = 'user-a';

/**
 * App with req.user injected (authenticated).
 */
function makeApp(userId: string, dataDir: string): express.Application {
  const app = express();
  app.use(express.json());
  app.use((req, _res, next) => {
    (req as any).user = { id: userId, role: 'user' };
    next();
  });
  app.use('/api/local/memory', createMemoryApi({ dataDir }));
  return app;
}

/**
 * App with no req.user — simulates missing auth.
 */
function makeUnauthApp(dataDir: string): express.Application {
  const app = express();
  app.use(express.json());
  // No req.user set; authActive defaults to true inside the router
  app.use('/api/local/memory', createMemoryApi({ dataDir }));
  return app;
}

// ── Setup / Teardown ───────────────────────────────────────────────────────────

describe('Memory API', () => {
  let tmpDir: string;
  let app: express.Application;

  beforeEach(() => {
    tmpDir = mkdtempSync(join(tmpdir(), 'memory-api-test-'));
    app = makeApp(USER_A, tmpDir);
  });

  afterEach(() => {
    rmSync(tmpDir, { recursive: true, force: true });
  });

  // ── GET /entries ─────────────────────────────────────────────────────────────

  describe('GET /entries', () => {
    it('returns empty entries and null index when no memory exists', async () => {
      const res = await request(app).get('/api/local/memory/entries');
      expect(res.status).toBe(200);
      expect(res.body.entries).toEqual([]);
      expect(res.body.index).toBeNull();
    });

    it('returns parsed entries and index when entries exist', async () => {
      // Seed directly via the memory helper
      upsertMemoryEntry(tmpDir, USER_A, {
        name: 'my-fact',
        type: 'user',
        description: 'A test fact',
        body: 'some body content',
      });

      const res = await request(app).get('/api/local/memory/entries');
      expect(res.status).toBe(200);

      const entries = res.body.entries as Array<{ name: string; description: string; type: string; body: string }>;
      expect(entries).toHaveLength(1);
      expect(entries[0]!.name).toBe('my-fact');
      expect(entries[0]!.description).toBe('A test fact');
      expect(entries[0]!.type).toBe('user');
      expect(entries[0]!.body.trim()).toBe('some body content');

      // Index should contain the entry line
      expect(typeof res.body.index).toBe('string');
      expect(res.body.index).toContain('my-fact');
    });

    it('returns 401 when the request is unauthenticated', async () => {
      const unauthApp = makeUnauthApp(tmpDir);
      const res = await request(unauthApp).get('/api/local/memory/entries');
      expect(res.status).toBe(401);
    });
  });

  // ── PUT /entries/:name ────────────────────────────────────────────────────────

  describe('PUT /entries/:name', () => {
    it('creates an entry and writes it to disk', async () => {
      const res = await request(app)
        .put('/api/local/memory/entries/my-note')
        .send({ description: 'A useful note', type: 'reference', body: 'Details here.' });

      expect(res.status).toBe(200);
      expect(res.body.ok).toBe(true);
      expect(res.body.name).toBe('my-note');

      // Verify file is on disk
      const factPath = join(tmpDir, USER_A, 'memory', 'my-note.md');
      expect(existsSync(factPath)).toBe(true);

      // Verify GET returns it
      const getRes = await request(app).get('/api/local/memory/entries');
      expect(getRes.status).toBe(200);
      const names = (getRes.body.entries as Array<{ name: string }>).map(e => e.name);
      expect(names).toContain('my-note');
    });

    it('updates an existing entry (upsert)', async () => {
      // Create first
      await request(app)
        .put('/api/local/memory/entries/upsert-me')
        .send({ description: 'Old description', type: 'user', body: 'old body' });

      // Update
      const res = await request(app)
        .put('/api/local/memory/entries/upsert-me')
        .send({ description: 'New description', type: 'feedback', body: 'new body' });

      expect(res.status).toBe(200);

      const getRes = await request(app).get('/api/local/memory/entries');
      const entry = (getRes.body.entries as Array<{ name: string; description: string; type: string }>)
        .find(e => e.name === 'upsert-me');
      expect(entry).toBeDefined();
      expect(entry!.description).toBe('New description');
      expect(entry!.type).toBe('feedback');
    });

    it('returns 400 + rejected_bad_name for a name that is too long', async () => {
      // Build a 65-char name using only URL-safe chars to avoid Express routing weirdness.
      // isValidMemoryName rejects names longer than 64 chars.
      const longName = 'a'.repeat(64) + 'b'; // 65 chars, all alphanumeric
      const res = await request(app)
        .put(`/api/local/memory/entries/${longName}`)
        .send({ description: 'desc', type: 'user', body: 'body' });
      expect(res.status).toBe(400);
      expect(res.body.error).toBe('rejected_bad_name');
    });

    it('returns 400 + rejected_unknown_type for invalid type', async () => {
      const res = await request(app)
        .put('/api/local/memory/entries/valid-name')
        .send({ description: 'A description', type: 'bogus-type', body: 'body content' });

      expect(res.status).toBe(400);
      expect(res.body.error).toBe('rejected_unknown_type');
    });

    it('returns 400 + rejected_body_too_large when body exceeds maxEntryBodyBytes', async () => {
      // Default maxEntryBodyBytes is 8192; send 9000 bytes
      const bigBody = 'x'.repeat(9000);
      const res = await request(app)
        .put('/api/local/memory/entries/big-entry')
        .send({ description: 'too big', type: 'user', body: bigBody });

      expect(res.status).toBe(400);
      expect(res.body.error).toBe('rejected_body_too_large');
    });

    it('returns 400 + rejected_bad_description for multi-line description', async () => {
      const res = await request(app)
        .put('/api/local/memory/entries/multi-line-desc')
        .send({ description: 'line one\nline two', type: 'user', body: 'body' });

      expect(res.status).toBe(400);
      expect(res.body.error).toBe('rejected_bad_description');
    });

    it('accepts all valid types', async () => {
      for (const type of ['user', 'feedback', 'project', 'reference'] as const) {
        const res = await request(app)
          .put(`/api/local/memory/entries/type-test-${type}`)
          .send({ description: `type is ${type}`, type, body: 'body' });
        expect(res.status).toBe(200);
      }
    });

    it('returns 401 when unauthenticated', async () => {
      const unauthApp = makeUnauthApp(tmpDir);
      const res = await request(unauthApp)
        .put('/api/local/memory/entries/my-note')
        .send({ description: 'desc', type: 'user', body: 'body' });
      expect(res.status).toBe(401);
    });
  });

  // ── DELETE /entries/:name ─────────────────────────────────────────────────────

  describe('DELETE /entries/:name', () => {
    it('removes an entry and updates the index', async () => {
      // Seed entry
      upsertMemoryEntry(tmpDir, USER_A, {
        name: 'delete-me',
        type: 'project',
        description: 'Temporary fact',
        body: 'will be deleted',
      });

      const res = await request(app).delete('/api/local/memory/entries/delete-me');
      expect(res.status).toBe(200);
      expect(res.body.ok).toBe(true);
      expect(res.body.name).toBe('delete-me');

      // Fact file should be gone from memory dir (moved to trash)
      const factPath = join(tmpDir, USER_A, 'memory', 'delete-me.md');
      expect(existsSync(factPath)).toBe(false);

      // GET should return empty entries
      const getRes = await request(app).get('/api/local/memory/entries');
      const names = (getRes.body.entries as Array<{ name: string }>).map(e => e.name);
      expect(names).not.toContain('delete-me');
    });

    it('returns 404 for a nonexistent entry', async () => {
      const res = await request(app).delete('/api/local/memory/entries/does-not-exist');
      expect(res.status).toBe(404);
    });

    it('returns 404 for an invalid name (do not leak existence)', async () => {
      // Name exceeding 64 chars → treated as 404 to not reveal system structure
      const res = await request(app).delete(`/api/local/memory/entries/${'z'.repeat(65)}`);
      expect(res.status).toBe(404);
    });

    it('returns 401 when unauthenticated', async () => {
      const unauthApp = makeUnauthApp(tmpDir);
      const res = await request(unauthApp).delete('/api/local/memory/entries/some-entry');
      expect(res.status).toBe(401);
    });
  });
});
