/**
 * Integration-style tests for /api/images + /api/gallery/images/:filename
 * (and DELETE /api/images). The routes now delegate to GalleryService,
 * so these tests build a fresh GalleryService pointed at a tmp dir and
 * exercise the routes end-to-end.
 */

import { jest, describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
import express from 'express';
import { createServer } from 'http';
import path from 'path';
import { promises as fs } from 'fs';
import os from 'os';

// Redirect userDataDir into a tmp dir so the gallery's writes don't
// touch the developer's real user data.
let tmpRoot;
jest.unstable_mockModule('../../utilities/userDataDir.js', () => ({
  getUserDataDir: () => tmpRoot,
  getUserDataPaths: () => ({
    base: tmpRoot,
    state: path.join(tmpRoot, 'state'),
    agents: path.join(tmpRoot, 'state', 'agents'),
    settings: path.join(tmpRoot, 'settings'),
    attachments: path.join(tmpRoot, 'attachments'),
    logs: path.join(tmpRoot, 'logs'),
    cache: path.join(tmpRoot, 'cache'),
    operations: path.join(tmpRoot, 'state', 'operations'),
    models: path.join(tmpRoot, 'state', 'models'),
    runtime: path.join(tmpRoot, 'runtime'),
    skills: path.join(tmpRoot, 'state', 'skills'),
    gallery: path.join(tmpRoot, 'gallery'),
    galleryImages: path.join(tmpRoot, 'gallery', 'images'),
    galleryVideos: path.join(tmpRoot, 'gallery', 'videos'),
  }),
  ensureUserDataDirs: async () => {},
}));

const { _resetGalleryServiceForTests, getGalleryService } = await import('../../services/galleryService.js');
const { getUserDataPaths } = await import('../../utilities/userDataDir.js');

const HTTP_STATUS = { OK: 200, BAD_REQUEST: 400, NOT_FOUND: 404, INTERNAL_SERVER_ERROR: 500 };

function buildApp() {
  const app = express();

  app.get('/api/images', async (req, res) => {
    try {
      const gallery = getGalleryService();
      const entries = await gallery.listImages({ agentId: req.query.agentId || undefined });
      const images = entries.map(e => ({
        ...e, size: e.bytes,
        url: `/api/gallery/images/${encodeURIComponent(e.filename)}`,
      }));
      res.json({ success: true, images });
    } catch (error) {
      res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ success: false, error: error.message });
    }
  });

  app.delete('/api/images', async (req, res) => {
    try {
      const { filename } = req.query;
      if (!filename) return res.status(HTTP_STATUS.BAD_REQUEST).json({ success: false, error: 'filename is required' });
      const ok = await getGalleryService().deleteImage(filename);
      if (!ok) return res.status(HTTP_STATUS.NOT_FOUND).json({ success: false, error: 'Image not found' });
      res.json({ success: true });
    } catch (error) {
      if (error.code === 'INVALID_FILENAME') return res.status(HTTP_STATUS.BAD_REQUEST).json({ success: false, error: 'Invalid filename' });
      if (error.code === 'INVALID_EXT') return res.status(HTTP_STATUS.BAD_REQUEST).json({ success: false, error: 'Unsupported file extension' });
      res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ success: false, error: error.message });
    }
  });

  app.get('/api/gallery/images/:filename', async (req, res) => {
    const filename = req.params.filename;
    const normalized = path.basename(filename);
    if (normalized !== filename || filename.includes('..')) {
      return res.status(HTTP_STATUS.BAD_REQUEST).json({ success: false, error: 'Invalid filename' });
    }
    const ext = path.extname(normalized).toLowerCase();
    if (!['.png', '.jpg', '.jpeg', '.webp', '.gif'].includes(ext)) {
      return res.status(HTTP_STATUS.BAD_REQUEST).json({ success: false, error: 'Unsupported extension' });
    }
    const target = path.join(getUserDataPaths().galleryImages, normalized);
    res.sendFile(target, (err) => {
      if (err && !res.headersSent) res.status(HTTP_STATUS.NOT_FOUND).json({ success: false, error: 'Image not found' });
    });
  });

  return app;
}

async function http(url, options = {}) {
  const resp = await globalThis.fetch(url, options);
  const body = resp.headers.get('content-type')?.includes('json') ? await resp.json() : await resp.text();
  return { status: resp.status, body, headers: resp.headers };
}

describe('gallery routes (GalleryService-backed)', () => {
  let server, baseUrl;

  async function saveImg(meta = {}, content = 'PNG-DATA') {
    const srcDir = path.join(tmpRoot, 'src');
    await fs.mkdir(srcDir, { recursive: true });
    const src = path.join(srcDir, `${meta.name || 'img'}.png`);
    await fs.writeFile(src, content);
    return await getGalleryService().saveImage({ sourcePath: src, metadata: meta });
  }

  beforeAll(async () => {
    tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gallery-routes-'));
    const app = buildApp();
    server = createServer(app);
    await new Promise(r => server.listen(0, r));
    baseUrl = `http://localhost:${server.address().port}`;
  });

  afterAll(async () => {
    server?.close();
    await fs.rm(tmpRoot, { recursive: true, force: true }).catch(() => {});
  });

  beforeEach(async () => {
    _resetGalleryServiceForTests();
    // Clean gallery dir between tests.
    const dir = path.join(tmpRoot, 'gallery', 'images');
    await fs.rm(dir, { recursive: true, force: true }).catch(() => {});
  });

  // ── GET /api/images ─────────────────────────────────────────

  it('returns empty list when gallery has no entries', async () => {
    const { status, body } = await http(`${baseUrl}/api/images`);
    expect(status).toBe(200);
    expect(body).toEqual({ success: true, images: [] });
  });

  it('lists saved gallery entries with sidecar metadata', async () => {
    await saveImg({ name: 'fox', prompt: 'a fox', model: 'dall-e-3', agentId: 'a', agentName: 'Alice' });
    const { body } = await http(`${baseUrl}/api/images`);
    expect(body.images).toHaveLength(1);
    const img = body.images[0];
    expect(img.prompt).toBe('a fox');
    expect(img.model).toBe('dall-e-3');
    expect(img.agentId).toBe('a');
    expect(img.url).toMatch(/^\/api\/gallery\/images\/.*fox\.png$/);
  });

  it('filters by agentId', async () => {
    await saveImg({ name: 'a', agentId: 'agent-A' });
    await saveImg({ name: 'b', agentId: 'agent-B' });
    const { body } = await http(`${baseUrl}/api/images?agentId=agent-B`);
    expect(body.images).toHaveLength(1);
    expect(body.images[0].agentId).toBe('agent-B');
  });

  // ── DELETE /api/images ──────────────────────────────────────

  it('rejects missing filename param', async () => {
    const { status } = await http(`${baseUrl}/api/images`, { method: 'DELETE' });
    expect(status).toBe(400);
  });

  it('rejects path traversal', async () => {
    const { status, body } = await http(
      `${baseUrl}/api/images?filename=..%2Fetc%2Fpasswd`,
      { method: 'DELETE' }
    );
    expect(status).toBe(400);
    expect(body.error).toMatch(/Invalid filename/);
  });

  it('rejects non-image extension', async () => {
    const { status, body } = await http(
      `${baseUrl}/api/images?filename=secret.txt`,
      { method: 'DELETE' }
    );
    expect(status).toBe(400);
    expect(body.error).toMatch(/Unsupported/);
  });

  it('returns 404 when the gallery entry does not exist', async () => {
    const { status } = await http(`${baseUrl}/api/images?filename=ghost.png`, { method: 'DELETE' });
    expect(status).toBe(404);
  });

  it('deletes an existing gallery entry + its sidecar', async () => {
    const saved = await saveImg({ prompt: 'deleting' });
    const { status, body } = await http(
      `${baseUrl}/api/images?filename=${encodeURIComponent(saved.filename)}`,
      { method: 'DELETE' }
    );
    expect(status).toBe(200);
    expect(body.success).toBe(true);
    await expect(fs.access(saved.path)).rejects.toBeTruthy();
    await expect(fs.access(`${saved.path}.json`)).rejects.toBeTruthy();
  });

  // ── GET /api/gallery/images/:filename ──────────────────────

  it('serves a gallery image by filename', async () => {
    const saved = await saveImg({ prompt: 'serve-me' }, 'PNG-BYTES');
    const resp = await globalThis.fetch(`${baseUrl}/api/gallery/images/${encodeURIComponent(saved.filename)}`);
    expect(resp.status).toBe(200);
    const buf = await resp.text();
    expect(buf).toBe('PNG-BYTES');
  });

  it('serve route rejects path traversal', async () => {
    const { status } = await http(`${baseUrl}/api/gallery/images/..%2Fetc%2Fshadow.png`);
    expect(status).toBe(400);
  });

  it('serve route returns 404 for unknown filename', async () => {
    const { status } = await http(`${baseUrl}/api/gallery/images/does-not-exist.png`);
    expect(status).toBe(404);
  });
});
