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

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function makeApp(userId: string, userFolderRoot: string): express.Application {
  const app = express();
  app.use((req, _res, next) => {
    (req as any).user = { id: userId, role: 'user' };
    next();
  });
  app.use('/api/users/me', createUserFolderApi({ userFolderRoot }));
  return app;
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('User Folder API — AGENTS.md routes', () => {
  let tmpRoot: string;
  let app: express.Application;
  const USER_A = 'user-a';

  beforeEach(() => {
    tmpRoot = mkdtempSync(join(tmpdir(), 'agents-md-test-'));
    app = makeApp(USER_A, tmpRoot);
  });

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

  // ── GET /agents-md ────────────────────────────────────────────────────────

  it('GET /agents-md returns exists=false when file is missing', async () => {
    const res = await request(app).get('/api/users/me/agents-md');
    expect(res.status).toBe(200);
    expect(res.body).toEqual({ exists: false, content: '' });
  });

  // ── PUT /agents-md ────────────────────────────────────────────────────────

  it('PUT /agents-md writes content; subsequent GET reflects it', async () => {
    const text = '# My Instructions\n\nAlways be concise.\n';

    const putRes = await request(app)
      .put('/api/users/me/agents-md')
      .set('Content-Type', 'text/plain')
      .send(text);
    expect(putRes.status).toBe(200);
    expect(putRes.body.ok).toBe(true);
    expect(typeof putRes.body.bytes).toBe('number');

    const getRes = await request(app).get('/api/users/me/agents-md');
    expect(getRes.status).toBe(200);
    expect(getRes.body.exists).toBe(true);
    expect(getRes.body.content).toBe(text);
  });

  it('PUT /agents-md with oversized body returns 413', async () => {
    // 64 KB + 1 byte — exceeds USER_AGENTS_MAX_BYTES
    const oversized = 'x'.repeat(64 * 1024 + 1);

    const res = await request(app)
      .put('/api/users/me/agents-md')
      .set('Content-Type', 'text/plain')
      .send(oversized);
    expect(res.status).toBe(413);
    expect(res.body.error).toMatch(/exceeds/);
  });

  // ── DELETE /agents-md ─────────────────────────────────────────────────────

  it('DELETE /agents-md removes the file', async () => {
    // First write something
    await request(app)
      .put('/api/users/me/agents-md')
      .set('Content-Type', 'text/plain')
      .send('# Hello\n');

    // Confirm it exists
    const beforeGet = await request(app).get('/api/users/me/agents-md');
    expect(beforeGet.body.exists).toBe(true);

    // Delete it
    const delRes = await request(app).delete('/api/users/me/agents-md');
    expect(delRes.status).toBe(200);
    expect(delRes.body.ok).toBe(true);
    expect(delRes.body.existed).toBe(true);

    // Confirm it's gone
    const afterGet = await request(app).get('/api/users/me/agents-md');
    expect(afterGet.body.exists).toBe(false);
  });

  it('DELETE /agents-md when file does not exist returns ok=true, existed=false', async () => {
    const res = await request(app).delete('/api/users/me/agents-md');
    expect(res.status).toBe(200);
    expect(res.body.ok).toBe(true);
    expect(res.body.existed).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// Spaces foundation: optional spaceId scoping (mirrors pieces-api)
//   - no spaceId          → per-user folder (byte-identical regression)
//   - case-space spaceId  → data/spaces/{id}/AGENTS.md
//   - unresolvable spaceId → 404, never silently falls back to user folder
// ---------------------------------------------------------------------------

describe('User Folder API — AGENTS.md spaceId scoping', () => {
  const USER_A = 'user-a';
  const CASE_SPACE = 'space-case-1';

  let tmpRoot: string;
  let userFolderRoot: string;

  /**
   * Stub resolver mirroring src/spaces/folder-resolver.ts semantics:
   *   personal → { rootDir: userFolderRoot, leafId: ownerId } (same user folder)
   *   case     → { rootDir: <userFolderRoot>/../spaces, leafId: spaceId }
   *   unknown  → null (not visible / not found)
   */
  function makeResolver(visibleIds: Set<string>) {
    const spacesRoot = join(dirname(userFolderRoot), 'spaces');
    return async (spaceId: string) => {
      if (!visibleIds.has(spaceId)) return null;
      if (spaceId === 'personal') return { rootDir: userFolderRoot, leafId: USER_A };
      return { rootDir: spacesRoot, leafId: spaceId };
    };
  }

  function makeSpaceApp(
    resolveSpaceFolder?: (spaceId: string) => Promise<{ rootDir: string; leafId: string } | null>,
  ): express.Application {
    const app = express();
    app.use((req, _res, next) => {
      (req as any).user = { id: USER_A, role: 'user' };
      next();
    });
    app.use('/api/users/me', createUserFolderApi({ userFolderRoot, resolveSpaceFolder }));
    return app;
  }

  beforeEach(() => {
    tmpRoot = mkdtempSync(join(tmpdir(), 'agents-md-space-'));
    userFolderRoot = join(tmpRoot, 'data', 'users');
  });

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

  // (b) regression — no spaceId hits the per-user folder
  it('without spaceId writes/reads under the per-user folder', async () => {
    const app = makeSpaceApp(makeResolver(new Set([CASE_SPACE])));
    const text = '# personal\n';

    const put = await request(app)
      .put('/api/users/me/agents-md')
      .set('Content-Type', 'text/plain')
      .send(text);
    expect(put.status).toBe(200);

    expect(existsSync(join(userFolderRoot, USER_A, 'AGENTS.md'))).toBe(true);
    expect(existsSync(join(tmpRoot, 'data', 'spaces', CASE_SPACE, 'AGENTS.md'))).toBe(false);

    const get = await request(app).get('/api/users/me/agents-md');
    expect(get.body).toEqual({ exists: true, content: text });
  });

  // (a) case-space spaceId → data/spaces/{id}/AGENTS.md
  it('with a case-space spaceId writes/reads/deletes under data/spaces/{id}', async () => {
    const app = makeSpaceApp(makeResolver(new Set([CASE_SPACE])));
    const text = '# shared space instructions\n';

    const put = await request(app)
      .put(`/api/users/me/agents-md?spaceId=${CASE_SPACE}`)
      .set('Content-Type', 'text/plain')
      .send(text);
    expect(put.status).toBe(200);

    const spaceFile = join(tmpRoot, 'data', 'spaces', CASE_SPACE, 'AGENTS.md');
    expect(existsSync(spaceFile)).toBe(true);
    expect(readFileSync(spaceFile, 'utf-8')).toBe(text);
    // The per-user folder must be untouched.
    expect(existsSync(join(userFolderRoot, USER_A, 'AGENTS.md'))).toBe(false);

    const get = await request(app).get(`/api/users/me/agents-md?spaceId=${CASE_SPACE}`);
    expect(get.body).toEqual({ exists: true, content: text });

    const del = await request(app).delete(`/api/users/me/agents-md?spaceId=${CASE_SPACE}`);
    expect(del.status).toBe(200);
    expect(del.body).toEqual({ ok: true, existed: true });
    expect(existsSync(spaceFile)).toBe(false);
  });

  // (c) unresolvable / forbidden spaceId → 404, no fallback
  it('returns 404 for a spaceId not visible to the viewer (GET/PUT/DELETE)', async () => {
    const app = makeSpaceApp(makeResolver(new Set([CASE_SPACE])));

    const get = await request(app).get('/api/users/me/agents-md?spaceId=nope');
    expect(get.status).toBe(404);

    const put = await request(app)
      .put('/api/users/me/agents-md?spaceId=nope')
      .set('Content-Type', 'text/plain')
      .send('# x\n');
    expect(put.status).toBe(404);
    expect(existsSync(join(userFolderRoot, USER_A, 'AGENTS.md'))).toBe(false);

    const del = await request(app).delete('/api/users/me/agents-md?spaceId=nope');
    expect(del.status).toBe(404);
  });
});
