import { describe, it, expect, vi } from 'vitest';
import request from 'supertest';
import { buildApp } from './server';
import { createSessionToken } from './auth-oidc/session';
import type { TeamDirectory } from './auth-oidc/types';
import type { PublishResult } from './backendClient';

const secret = new TextEncoder().encode('test-secret-at-least-32-bytes-long-ok');
const backend = { baseUrl: 'http://backend.invalid', secret: 'shared-secret' };

function buildTestApp(overrides?: {
  teamDirectory?: TeamDirectory;
  publish?: (...args: unknown[]) => Promise<PublishResult>;
}) {
  const teamDirectory: TeamDirectory = overrides?.teamDirectory ?? {
    getTeamsForUser: vi.fn().mockResolvedValue(['progcom']),
  };
  return buildApp({
    teamDirectory,
    sessionSecret: secret,
    backend,
    publicBaseUrl: 'http://upstream.invalid',
    publish: overrides?.publish as any,
  });
}

async function sessionCookie(username = 'j47ho') {
  const token = await createSessionToken({ username }, secret);
  return `eventer_session=${token}`;
}

describe('GET /submit (protected route)', () => {
  it('redirects to /login without a session', async () => {
    const app = buildTestApp();
    const res = await request(app).get('/submit');
    expect(res.status).toBe(302);
    expect(res.headers.location).toBe('/login');
  });

  it('renders the submission form for a session on an allowed team', async () => {
    const app = buildTestApp();
    const res = await request(app).get('/submit').set('Cookie', [await sessionCookie('j47ho')]);
    expect(res.status).toBe(200);
    expect(res.headers['content-type']).toMatch(/html/);
    expect(res.text).toContain('j47ho');
    expect(res.text).toContain('id="event-form"');
  });

  it('returns 403 when the session is valid but the user is not on a candidate team', async () => {
    const teamDirectory: TeamDirectory = { getTeamsForUser: vi.fn().mockResolvedValue([]) };
    const app = buildTestApp({ teamDirectory });
    const res = await request(app).get('/submit').set('Cookie', [await sessionCookie('someoneelse')]);
    expect(res.status).toBe(403);
  });
});

describe('POST /submit', () => {
  const validBody = {
    name: 'Test Event',
    short: 'A short description',
    startLocal: '2026-08-05T10:00',
    endLocal: null,
    online: false,
    location: 'DC 1351',
    descriptionMarkdown: 'Some description',
    registerLink: null,
    poster: null,
  };

  it('requires a session', async () => {
    const app = buildTestApp();
    const res = await request(app).post('/submit').send(validBody);
    expect(res.status).toBe(302);
  });

  it('calls the injected publish function with a well-formed payload and returns its result', async () => {
    const publish = vi.fn().mockResolvedValue({
      ok: true,
      status: 201,
      body: { status: 'pr_created', pullRequestUrl: 'https://git.example/pulls/1' },
    });
    const app = buildTestApp({ publish });
    const res = await request(app).post('/submit').set('Cookie', [await sessionCookie('j47ho')]).send(validBody);

    expect(res.status).toBe(201);
    expect(res.body.ok).toBe(true);
    expect(res.body.pullRequestUrl).toBe('https://git.example/pulls/1');

    expect(publish).toHaveBeenCalledTimes(1);
    const [calledBackend, calledPayload] = publish.mock.calls[0];
    expect(calledBackend).toEqual(backend);
    expect(calledPayload.submittedByEmail).toBe('j47ho@uwaterloo.ca');
    expect(calledPayload.event.name).toBe('Test Event');
    expect(calledPayload.event.slug).toBe('Test-Event');
    expect(calledPayload.event.startDate).toBe('August 05 2026 10:00');
    expect(calledPayload.event.year).toBe(2026);
    expect(calledPayload.event.term).toBe('spring');
  });

  it('rejects a missing required field before ever calling publish', async () => {
    const publish = vi.fn();
    const app = buildTestApp({ publish });
    const res = await request(app)
      .post('/submit')
      .set('Cookie', [await sessionCookie('j47ho')])
      .send({ ...validBody, name: '' });

    expect(res.status).toBe(400);
    expect(publish).not.toHaveBeenCalled();
  });

  it('surfaces backend validation errors instead of a generic failure', async () => {
    const publish = vi.fn().mockResolvedValue({
      ok: false,
      status: 400,
      body: { error: { code: 'VALIDATION_ERROR', message: 'event.slug must match pattern ...' } },
    });
    const app = buildTestApp({ publish });
    const res = await request(app).post('/submit').set('Cookie', [await sessionCookie('j47ho')]).send(validBody);

    expect(res.status).toBe(400);
    expect(res.body.ok).toBe(false);
    expect(res.body.message).toBe('event.slug must match pattern ...');
  });
});

describe('POST /upload', () => {
  it('requires a session', async () => {
    const app = buildTestApp();
    const res = await request(app).post('/upload');
    expect(res.status).toBe(302);
  });

  it('rejects a request with no file', async () => {
    const app = buildTestApp();
    const res = await request(app).post('/upload').set('Cookie', [await sessionCookie('j47ho')]);
    expect(res.status).toBe(400);
    expect(res.body.error.code).toBe('MISSING_FILE');
  });

  it('accepts a png upload and returns a sourceUrl under the configured public base URL', async () => {
    const app = buildTestApp();
    const res = await request(app)
      .post('/upload')
      .set('Cookie', [await sessionCookie('j47ho')])
      .attach('poster', Buffer.from([0x89, 0x50, 0x4e, 0x47]), { filename: 'poster.png', contentType: 'image/png' });

    expect(res.status).toBe(200);
    expect(res.body.sourceUrl).toMatch(/^http:\/\/upstream\.invalid\/uploads\/[0-9a-f]+\.png$/);
    expect(res.body.contentType).toBe('image/png');
    // The response's filename must be the actual safe generated name, never
    // the original filename the uploader picked -- see uploadRoute.ts.
    expect(res.body.filename).not.toContain('poster');
  });

  it('rejects a disallowed content type', async () => {
    const app = buildTestApp();
    const res = await request(app)
      .post('/upload')
      .set('Cookie', [await sessionCookie('j47ho')])
      .attach('poster', Buffer.from('not an image'), { filename: 'evil.txt', contentType: 'text/plain' });

    expect(res.status).toBe(400);
    expect(res.body.error.code).toBe('UPLOAD_REJECTED');
  });
});
