/**
 * Unit tests for the /api/images/:sessionId/:filename endpoint.
 *
 * Covers:
 *  - Path traversal rejection
 *  - Serving images from project dir (with and without active session)
 *  - Serving images from OS temp dir
 *  - Serving images from agent working directories
 *  - 404 when image doesn't exist anywhere
 *  - Content-type correctness for different extensions
 */

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

// ── Minimal WebServer stand-in ──────────────────────────────────
// We only mount the single route under test so the full webServer
// dependency tree isn't required.

const HTTP_STATUS = { BAD_REQUEST: 400, NOT_FOUND: 404, INTERNAL_SERVER_ERROR: 500, FORBIDDEN: 403 };

function buildApp({ sessions, orchestrator, projectDir, logger }) {
  const app = express();

  // Replicate the exact route from webServer.js
  app.get('/api/images/:sessionId/:filename', async (req, res) => {
    try {
      const { sessionId, filename } = req.params;

      const normalizedFilename = path.basename(filename);
      if (normalizedFilename !== filename || filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
        return res.status(HTTP_STATUS.BAD_REQUEST).json({ success: false, error: 'Invalid filename' });
      }

      const session = sessions.get(sessionId);
      const effectiveProjectDir = session?.projectDir || projectDir || process.cwd();

      let imagePath = null;
      const searchPaths = [
        path.join(effectiveProjectDir, 'images', normalizedFilename),
        path.join('/tmp/loxia-images', sessionId, normalizedFilename),
        path.join('/tmp/loxia-images', normalizedFilename),
        path.join(process.env.TEMP || process.env.TMP || '/tmp', 'loxia-images', sessionId, normalizedFilename),
        path.join(process.env.TEMP || process.env.TMP || '/tmp', 'loxia-images', normalizedFilename)
      ];

      if (orchestrator?.agentPool) {
        try {
          const agents = await orchestrator.agentPool.getAllAgents();
          for (const agent of agents) {
            if (agent.directoryAccess?.workingDirectory) {
              searchPaths.push(path.join(agent.directoryAccess.workingDirectory, 'images', normalizedFilename));
            }
          }
        } catch (_) { /* ignore */ }
      }

      for (const sp of searchPaths) {
        try {
          const stats = await fs.stat(sp);
          if (stats.isFile()) { imagePath = sp; break; }
        } catch (_) { continue; }
      }

      if (!imagePath) {
        return res.status(HTTP_STATUS.NOT_FOUND).json({ success: false, error: 'Image not found' });
      }

      res.sendFile(imagePath, (err) => {
        if (err && !res.headersSent) {
          res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ success: false, error: 'Failed to serve image' });
        }
      });
    } catch (error) {
      if (!res.headersSent) {
        res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ success: false, error: error.message });
      }
    }
  });

  return app;
}

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

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

// ── Test suite ──────────────────────────────────────────────────

describe('/api/images/:sessionId/:filename', () => {
  let server, baseUrl;
  let tmpProjectDir;
  let tmpAgentDir;
  const sessions = new Map();
  const sessionId = 'test-session-abc123';

  beforeAll(async () => {
    // Create temp directories that simulate project and agent dirs
    tmpProjectDir = path.join(os.tmpdir(), `loxia-img-test-proj-${Date.now()}`);
    tmpAgentDir = path.join(os.tmpdir(), `loxia-img-test-agent-${Date.now()}`);
    await fs.mkdir(path.join(tmpProjectDir, 'images'), { recursive: true });
    await fs.mkdir(path.join(tmpAgentDir, 'images'), { recursive: true });

    // Also create the OS temp search path
    const osTempPath = path.join(process.env.TEMP || process.env.TMP || '/tmp', 'loxia-images', sessionId);
    await fs.mkdir(osTempPath, { recursive: true });

    // Write test images
    const pngHeader = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); // PNG magic bytes
    await fs.writeFile(path.join(tmpProjectDir, 'images', 'project-image.png'), pngHeader);
    await fs.writeFile(path.join(tmpAgentDir, 'images', 'agent-image.png'), pngHeader);
    await fs.writeFile(path.join(osTempPath, 'temp-image.png'), pngHeader);

    // Register session
    sessions.set(sessionId, { projectDir: tmpProjectDir });

    const orchestrator = {
      agentPool: {
        getAllAgents: async () => [
          { directoryAccess: { workingDirectory: tmpAgentDir } }
        ]
      }
    };

    const app = buildApp({
      sessions,
      orchestrator,
      projectDir: tmpProjectDir,
      logger: { info() {}, warn() {}, error() {}, debug() {} }
    });

    server = createServer(app);
    await new Promise(resolve => server.listen(0, resolve));
    const port = server.address().port;
    baseUrl = `http://localhost:${port}`;
  });

  afterAll(async () => {
    server?.close();
    await fs.rm(tmpProjectDir, { recursive: true, force: true }).catch(() => {});
    await fs.rm(tmpAgentDir, { recursive: true, force: true }).catch(() => {});
    const osTempPath = path.join(process.env.TEMP || process.env.TMP || '/tmp', 'loxia-images', sessionId);
    await fs.rm(osTempPath, { recursive: true, force: true }).catch(() => {});
  });

  // ── Security ──────────────────────────────────────────────

  it('rejects path traversal with ..', async () => {
    const { status, body } = await fetch(`${baseUrl}/api/images/${sessionId}/..%2F..%2Fetc%2Fpasswd`);
    expect(status).toBe(400);
    expect(body.error).toBe('Invalid filename');
  });

  it('rejects filenames with slashes', async () => {
    const { status, body } = await fetch(`${baseUrl}/api/images/${sessionId}/sub%2Ffile.png`);
    expect(status).toBe(400);
    expect(body.error).toBe('Invalid filename');
  });

  it('rejects filenames with backslashes', async () => {
    const { status, body } = await fetch(`${baseUrl}/api/images/${sessionId}/sub%5Cfile.png`);
    expect(status).toBe(400);
    expect(body.error).toBe('Invalid filename');
  });

  // ── Serving from project directory (session active) ───────

  it('serves image from project/images/ when session is active', async () => {
    const { status, headers } = await fetch(`${baseUrl}/api/images/${sessionId}/project-image.png`);
    expect(status).toBe(200);
    expect(headers.get('content-type')).toMatch(/image|octet/);
  });

  // ── Serving without active session (session expired) ──────

  it('still serves image when session has expired (falls back to cwd)', async () => {
    // Use a sessionId that doesn't exist in the map
    // Image is in the OS temp dir under the expired sessionId
    const expiredSessionId = sessionId; // temp image was created under this id
    // Remove session temporarily
    sessions.delete(sessionId);

    // The image is in OS temp: $TEMP/loxia-images/test-session-abc123/temp-image.png
    const { status } = await fetch(`${baseUrl}/api/images/${expiredSessionId}/temp-image.png`);
    expect(status).toBe(200);

    // Restore session for other tests
    sessions.set(sessionId, { projectDir: tmpProjectDir });
  });

  // ── Serving from agent working directory ──────────────────

  it('finds image in agent working directory', async () => {
    const { status } = await fetch(`${baseUrl}/api/images/${sessionId}/agent-image.png`);
    expect(status).toBe(200);
  });

  // ── Serving from OS temp directory ────────────────────────

  it('finds image in OS temp loxia-images directory', async () => {
    const { status } = await fetch(`${baseUrl}/api/images/${sessionId}/temp-image.png`);
    expect(status).toBe(200);
  });

  // ── 404 for missing images ────────────────────────────────

  it('returns 404 for non-existent image', async () => {
    const { status, body } = await fetch(`${baseUrl}/api/images/${sessionId}/does-not-exist.png`);
    expect(status).toBe(404);
    expect(body.error).toBe('Image not found');
  });

  it('returns 404 for unknown session + missing file', async () => {
    const { status, body } = await fetch(`${baseUrl}/api/images/unknown-session-xyz/nope.png`);
    expect(status).toBe(404);
    expect(body.error).toBe('Image not found');
  });
});
