import { test, expect } from '@playwright/test';

const BASE = process.env.BASE_URL || 'https://marketing-stage-r4tfklscwq-uc.a.run.app';

async function get(path: string, headers: Record<string, string> = {}) {
  return fetch(`${BASE}${path}`, { headers, redirect: 'manual' });
}

async function post(path: string, body: unknown, headers: Record<string, string> = {}) {
  return fetch(`${BASE}${path}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', ...headers },
    body: JSON.stringify(body),
  });
}

// ── Security headers ──────────────────────────────────────────────────────────
// All HTML pages and API responses carry the full security header suite.
// The El runtime's handle_request wrapper applies sec_headers_json() to every
// response, so we can assert the same set on both HTML pages and JSON APIs.

test.describe('Security headers', () => {
  const htmlPages = ['/', '/about', '/checkout?plan=professional'];
  for (const path of htmlPages) {
    test(`HTML ${path} — required security headers present`, async () => {
      const r = await get(path);
      expect(r.headers.get('x-content-type-options')).toBe('nosniff');
      expect(r.headers.get('x-frame-options')).toMatch(/DENY|SAMEORIGIN/i);
      expect(r.headers.get('referrer-policy')).toBeTruthy();
      expect(r.headers.get('content-security-policy')).toBeTruthy();
    });
  }

  test('API responses carry x-content-type-options', async () => {
    const r = await get('/api/health');
    expect(r.headers.get('x-content-type-options')).toBe('nosniff');
  });

  test('permissions-policy header is present', async () => {
    const r = await get('/');
    expect(r.headers.get('permissions-policy')).toBeTruthy();
  });
});

// ── CORS enforcement on /api/supabase-config ──────────────────────────────────
// This endpoint enforces an explicit origin allowlist:
//   - empty Origin (server-side / curl): BLOCKED (403)
//   - https://neurontechnologies.ai: ALLOWED
//   - https://www.neurontechnologies.ai: ALLOWED
//   - http://localhost:*: ALLOWED (dev)
//   - anything else (e.g. evil.com): BLOCKED (403)

test.describe('CORS enforcement — /api/supabase-config', () => {
  test('Allows requests with no Origin header (same-origin browser fetches)', async () => {
    // Same-origin browser fetches (e.g. checkout page fetching supabase-config on
    // the same domain) do not send an Origin header. The server must pass these
    // through — blocking them would break the checkout flow on production.
    // Server-side exfiltration is prevented by the evil-origin 403 below.
    const r = await get('/api/supabase-config');
    expect(r.status).toBe(200);
  });

  test('Rejects evil origin', async () => {
    const r = await get('/api/supabase-config', { Origin: 'https://evil.com' });
    expect(r.status).toBe(403);
  });

  test('Allows neurontechnologies.ai origin', async () => {
    const r = await get('/api/supabase-config', { Origin: 'https://neurontechnologies.ai' });
    expect(r.status).toBe(200);
    const body = await r.json() as Record<string, string>;
    expect(body.url).toMatch(/supabase\.co/);
    expect(typeof body.anon_key).toBe('string');
    expect(body.anon_key.length).toBeGreaterThan(20);
  });

  test('Allows www.neurontechnologies.ai origin', async () => {
    const r = await get('/api/supabase-config', { Origin: 'https://www.neurontechnologies.ai' });
    expect(r.status).toBe(200);
  });

  test('Allows localhost origin (dev)', async () => {
    const r = await get('/api/supabase-config', { Origin: 'http://localhost:3001' });
    expect(r.status).toBe(200);
  });
});

// ── Auth enforcement on /api/demo ─────────────────────────────────────────────
// All requests require a valid Supabase access_token.
// Missing or invalid tokens return {"auth_required":true}.

test.describe('Auth enforcement — /api/demo', () => {
  test('Rejects POST with no access_token', async () => {
    const r = await post('/api/demo', { message: 'hello' });
    const body = await r.json() as Record<string, unknown>;
    expect(body.auth_required).toBe(true);
  });

  test('Rejects POST with invalid access_token', async () => {
    const r = await post('/api/demo', { message: 'hello', access_token: 'invalid.token.here' });
    const body = await r.json() as Record<string, unknown>;
    expect(body.auth_required).toBe(true);
  });

  test('Rejects empty message (length guard fires after auth check)', async () => {
    // With no token, auth check fires first
    const r = await post('/api/demo', { message: '', access_token: 'invalid' });
    const body = await r.json() as Record<string, unknown>;
    expect(body.auth_required || body.error).toBeTruthy();
  });
});

// ── Stripe webhook signature enforcement ──────────────────────────────────────

test.describe('Stripe webhook security', () => {
  test('Rejects POST with no Stripe-Signature header', async () => {
    const r = await post('/api/webhooks/stripe', {
      type: 'payment_intent.succeeded',
      data: { object: { amount: 9900 } },
    });
    expect(r.status).toBe(400);
  });

  test('Rejects POST with malformed Stripe-Signature', async () => {
    const r = await post(
      '/api/webhooks/stripe',
      { type: 'payment_intent.succeeded', data: { object: {} } },
      { 'Stripe-Signature': 't=1234,v1=fakesignature' },
    );
    expect(r.status).toBe(400);
  });
});

// ── Information leakage — source and build files must not be exposed ──────────
// The Docker image copies only compiled artifacts and static assets into
// /srv/landing/. Source files (.el, Makefile, Dockerfile) never land there,
// so all these paths should 404.

test.describe('Information leakage — source files not served', () => {
  const leakyPaths = [
    '/src/main.el',
    '/.env',
    '/Dockerfile.stage',
    '/runtime/el_runtime.c',
    '/.gitea/workflows/stage.yaml',
    '/dist/neuron-landing',
  ];
  for (const path of leakyPaths) {
    test(`${path} returns 404`, async () => {
      const r = await get(path);
      expect(r.status).toBe(404);
    });
  }
});

// ── /api/soul-health — internal-only diagnostic ───────────────────────────────
// Returns 404 without the X-Internal: true header.
// Returns 200 with the header (allows in-container health probing).

test.describe('Soul health — internal gate', () => {
  test('Returns 404 without X-Internal header', async () => {
    const r = await get('/api/soul-health');
    expect(r.status).toBe(404);
  });

  test('Returns 200 with X-Internal: true and includes soul_url', async () => {
    const r = await get('/api/soul-health', { 'X-Internal': 'true' });
    expect(r.status).toBe(200);
    // The response embeds raw probe output which may contain literal newlines
    // inside JSON strings (invalid JSON). Check via text search to avoid
    // JSON.parse failure on the control characters.
    const text = await r.text();
    expect(text).toContain('"soul_url"');
    expect(text).toMatch(/soul_url.*https?:\/\//);
  });
});

// ── Path traversal ────────────────────────────────────────────────────────────
// The El runtime only serves files from whitelisted paths (src/assets/,
// src/shares/, src/js/).  Any traversal attempt resolves to 404 — the
// runtime never reads outside its served directories.

test.describe('Path traversal blocked', () => {
  const traversals = [
    '/assets/../../../etc/passwd',
    '/assets/%2e%2e%2f%2e%2e%2fetc%2fpasswd',
    '/js/../../../etc/passwd',
  ];
  for (const path of traversals) {
    test(`Traversal blocked: ${path}`, async () => {
      const r = await get(path);
      expect(r.status).toBe(404);
      const text = await r.text();
      // Must not contain any /etc/passwd content
      expect(text).not.toContain('root:');
    });
  }
});

// ── Input validation — /api/demo message length cap ──────────────────────────
// Messages over 8000 chars are rejected before any auth or LLM call.

test.describe('Input validation', () => {
  test('Oversized message (>8000 chars) is rejected with error', async () => {
    const r = await post('/api/demo', {
      message: 'A'.repeat(10000),
      access_token: 'test',
    });
    const body = await r.json() as Record<string, unknown>;
    // Length guard fires before auth check in server code
    expect(typeof body.error).toBe('string');
    expect((body.error as string).toLowerCase()).toMatch(/long|length|8000/i);
  });
});
