import { describe, it, expect, beforeEach } from 'vitest';
import express from 'express';
import request from 'supertest';
import { mkdtempSync, writeFileSync, existsSync, readdirSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { ConfigManager } from '../config-manager.js';
import { mountBrandingApi, resolveBranding } from './branding-api.js';

function makeApp(yaml: string, withUpload = false) {
  const dir = mkdtempSync(join(tmpdir(), 'branding-api-'));
  writeFileSync(join(dir, 'config.yaml'), yaml);
  const cm = new ConfigManager(join(dir, 'config.yaml'));
  const app = express();
  if (withUpload) {
    mountBrandingApi(app, cm, {
      brandingDir: join(dir, 'branding'),
      adminGuard: (_req, _res, next) => next(),
    });
  } else {
    mountBrandingApi(app, cm);
  }
  return { app, cm, dir };
}

describe('Branding API', () => {
  it('returns defaults when branding is not configured', async () => {
    const { app } = makeApp('provider:\n  model: test-model\n');
    const res = await request(app).get('/api/branding');
    expect(res.status).toBe(200);
    expect(res.body).toEqual({
      appName: 'MAESTRO',
      primaryColor: '#2563eb',
      loginPageTitle: 'MAESTRO',
      logoUrl: null,
      faviconUrl: null,
      footerText: null,
    });
  });

  it('returns configured values when branding is set', async () => {
    const { app } = makeApp([
      'provider:',
      '  model: test-model',
      'branding:',
      '  app_name: "My Team AI"',
      '  primary_color: "#ff5500"',
      '  login_page_title: "Welcome to My Team"',
      '  logo_url: "/branding/logo-abc.svg"',
      '  favicon_url: "/branding/favicon-def.png"',
      '  footer_text: "© 2026 My Team"',
    ].join('\n'));
    const res = await request(app).get('/api/branding');
    expect(res.status).toBe(200);
    expect(res.body).toEqual({
      appName: 'My Team AI',
      primaryColor: '#ff5500',
      loginPageTitle: 'Welcome to My Team',
      logoUrl: '/branding/logo-abc.svg',
      faviconUrl: '/branding/favicon-def.png',
      footerText: '© 2026 My Team',
    });
  });

  it('falls back loginPageTitle to appName when only appName is set', async () => {
    const { app } = makeApp([
      'provider:',
      '  model: test-model',
      'branding:',
      '  app_name: "Custom App"',
    ].join('\n'));
    const res = await request(app).get('/api/branding');
    expect(res.body.appName).toBe('Custom App');
    expect(res.body.loginPageTitle).toBe('Custom App');
    expect(res.body.primaryColor).toBe('#2563eb');
  });

  it('ignores empty strings and falls back to defaults', async () => {
    const { app } = makeApp([
      'provider:',
      '  model: test-model',
      'branding:',
      '  app_name: ""',
      '  primary_color: "   "',
    ].join('\n'));
    const res = await request(app).get('/api/branding');
    expect(res.body.appName).toBe('MAESTRO');
    expect(res.body.primaryColor).toBe('#2563eb');
  });

  it('resolveBranding returns defaults for undefined configManager', () => {
    const branding = resolveBranding(undefined);
    expect(branding).toEqual({
      appName: 'MAESTRO',
      primaryColor: '#2563eb',
      loginPageTitle: 'MAESTRO',
      logoUrl: null,
      faviconUrl: null,
      footerText: null,
    });
  });

  describe('reactive to config updates', () => {
    let cm: ConfigManager;
    let app: express.Application;

    beforeEach(() => {
      const built = makeApp('provider:\n  model: test-model\n');
      cm = built.cm;
      app = built.app;
    });

    it('reflects runtime updates via ConfigManager', async () => {
      const before = await request(app).get('/api/branding');
      expect(before.body.appName).toBe('MAESTRO');

      const etag = cm.getConfigForApi().etag;
      cm.updateConfig({ branding: { appName: 'Hot Reload' } }, etag);

      const after = await request(app).get('/api/branding');
      expect(after.body.appName).toBe('Hot Reload');
    });
  });

  describe('asset upload', () => {
    // 1x1 transparent PNG
    const PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';

    it('uploads a logo and reflects it in GET /api/branding', async () => {
      const { app, dir } = makeApp('provider:\n  model: test-model\n', true);

      const upload = await request(app)
        .post('/api/branding/upload')
        .send({ kind: 'logo', filename: 'my-logo.png', contentBase64: PNG_BASE64 });
      expect(upload.status).toBe(200);
      expect(upload.body.ok).toBe(true);
      expect(upload.body.url).toMatch(/^\/branding\/logo-[a-f0-9]{12}\.png$/);

      // File actually written to branding dir
      expect(existsSync(join(dir, 'branding'))).toBe(true);
      const files = readdirSync(join(dir, 'branding'));
      expect(files.some(f => f.startsWith('logo-') && f.endsWith('.png'))).toBe(true);

      // Reflected in public GET
      const get = await request(app).get('/api/branding');
      expect(get.body.logoUrl).toBe(upload.body.url);
    });

    it('serves /branding assets with CSP sandbox + nosniff headers (SVG XSS hardening)', async () => {
      const { app } = makeApp('provider:\n  model: test-model\n', true);

      // SVG with an embedded script — allowed by the extension allowlist
      const svg = '<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>';
      const upload = await request(app)
        .post('/api/branding/upload')
        .send({ kind: 'logo', filename: 'logo.svg', contentBase64: Buffer.from(svg).toString('base64') });
      expect(upload.status).toBe(200);

      const asset = await request(app).get(upload.body.url);
      expect(asset.status).toBe(200);
      expect(asset.headers['content-security-policy']).toBe('sandbox');
      expect(asset.headers['x-content-type-options']).toBe('nosniff');
      // Content-Type must stay image/svg+xml so <img src> rendering still works
      expect(asset.headers['content-type']).toMatch(/image\/svg\+xml/);
    });

    it('rejects invalid kind', async () => {
      const { app } = makeApp('provider:\n  model: test-model\n', true);
      const res = await request(app)
        .post('/api/branding/upload')
        .send({ kind: 'banner', filename: 'x.png', contentBase64: PNG_BASE64 });
      expect(res.status).toBe(400);
    });

    it('rejects disallowed extension for favicon', async () => {
      const { app } = makeApp('provider:\n  model: test-model\n', true);
      const res = await request(app)
        .post('/api/branding/upload')
        .send({ kind: 'favicon', filename: 'evil.gif', contentBase64: PNG_BASE64 });
      expect(res.status).toBe(400);
      expect(res.body.error).toMatch(/extension/);
    });

    it('rejects files over size limit', async () => {
      const { app } = makeApp('provider:\n  model: test-model\n', true);
      const big = Buffer.alloc(257 * 1024, 0x00).toString('base64'); // > 256KB for favicon
      const res = await request(app)
        .post('/api/branding/upload')
        .send({ kind: 'favicon', filename: 'big.png', contentBase64: big });
      expect(res.status).toBe(413);
    });

    it('DELETE clears the config field and removes the file', async () => {
      const { app, dir } = makeApp('provider:\n  model: test-model\n', true);

      const upload = await request(app)
        .post('/api/branding/upload')
        .send({ kind: 'logo', filename: 'my.png', contentBase64: PNG_BASE64 });
      expect(upload.status).toBe(200);
      const uploadedName = upload.body.url.replace('/branding/', '');
      expect(existsSync(join(dir, 'branding', uploadedName))).toBe(true);

      const del = await request(app).delete('/api/branding/upload?kind=logo');
      expect(del.status).toBe(200);
      expect(existsSync(join(dir, 'branding', uploadedName))).toBe(false);

      const get = await request(app).get('/api/branding');
      expect(get.body.logoUrl).toBeNull();
    });

    it('replacing an existing asset cleans up the old file', async () => {
      const { app, dir } = makeApp('provider:\n  model: test-model\n', true);

      const first = await request(app)
        .post('/api/branding/upload')
        .send({ kind: 'logo', filename: 'a.png', contentBase64: PNG_BASE64 });
      const firstName = first.body.url.replace('/branding/', '');

      const second = await request(app)
        .post('/api/branding/upload')
        .send({ kind: 'logo', filename: 'b.png', contentBase64: PNG_BASE64 });
      const secondName = second.body.url.replace('/branding/', '');

      expect(firstName).not.toBe(secondName);
      const files = readdirSync(join(dir, 'branding')).filter(f => f.startsWith('logo-'));
      expect(files).toEqual([secondName]);
    });
  });
});
