import type { NextFunction, Request, Response } from 'express';
import type { Logger } from 'nestjs-pino';
import { createCsrfMiddleware } from './csrf.middleware';

interface SessionStub {
  user?: { oid: string };
  csrfToken?: string;
}

function makeReq(opts: {
  method: string;
  path: string;
  headers?: Record<string, string>;
  session?: SessionStub;
}): Request {
  const headers = opts.headers ?? {};
  return {
    method: opts.method,
    path: opts.path,
    session: opts.session,
    get: (name: string) => headers[name],
  } as unknown as Request;
}

function makeRes() {
  const res = {
    status: jest.fn(),
    json: jest.fn(),
  };
  res.status.mockReturnValue(res);
  res.json.mockReturnValue(res);
  return res as unknown as Response & { status: jest.Mock; json: jest.Mock };
}

function makeLogger() {
  return {
    log: jest.fn(),
    warn: jest.fn(),
    error: jest.fn(),
    debug: jest.fn(),
  } as unknown as Logger & { warn: jest.Mock };
}

describe('csrf middleware', () => {
  const next: NextFunction = jest.fn();
  beforeEach(() => (next as jest.Mock).mockReset());

  describe('skip cases', () => {
    it.each(['GET', 'HEAD', 'OPTIONS'])('passes through safe method %s', (method) => {
      const csrf = createCsrfMiddleware(makeLogger());
      const req = makeReq({ method, path: '/api/anything' });
      const res = makeRes();
      csrf(req, res, next);
      expect(next).toHaveBeenCalledTimes(1);
      expect(res.status).not.toHaveBeenCalled();
    });

    it('passes through anonymous mutating requests (no req.session.user)', () => {
      const csrf = createCsrfMiddleware(makeLogger());
      const req = makeReq({ method: 'POST', path: '/api/public', session: {} });
      const res = makeRes();
      csrf(req, res, next);
      expect(next).toHaveBeenCalledTimes(1);
    });

    it('passes through /api/auth/login (CSRF-exempt: session is born here)', () => {
      const csrf = createCsrfMiddleware(makeLogger());
      const req = makeReq({
        method: 'POST',
        path: '/api/auth/login',
        session: { user: { oid: 'u' }, csrfToken: 'irrelevant' },
      });
      const res = makeRes();
      csrf(req, res, next);
      expect(next).toHaveBeenCalledTimes(1);
    });

    it('passes through /api/auth/callback (CSRF-exempt)', () => {
      const csrf = createCsrfMiddleware(makeLogger());
      const req = makeReq({
        method: 'POST',
        path: '/api/auth/callback',
        session: { user: { oid: 'u' }, csrfToken: 'irrelevant' },
      });
      const res = makeRes();
      csrf(req, res, next);
      expect(next).toHaveBeenCalledTimes(1);
    });
  });

  describe('enforcement', () => {
    it('rejects with 403 when an authenticated mutation lacks X-CSRF-Token', () => {
      const logger = makeLogger();
      const csrf = createCsrfMiddleware(logger);
      const req = makeReq({
        method: 'POST',
        path: '/api/profile',
        session: { user: { oid: 'u' }, csrfToken: 'expected-token' },
      });
      const res = makeRes();
      csrf(req, res, next);
      expect(res.status).toHaveBeenCalledWith(403);
      expect(res.json).toHaveBeenCalledWith(
        expect.objectContaining({
          error: expect.objectContaining({
            code: 'csrf',
            message: 'CSRF token missing or invalid',
          }),
        }),
      );
      expect(next).not.toHaveBeenCalled();
      expect(logger.warn).toHaveBeenCalledWith(
        expect.objectContaining({ event: 'csrf.reject', hasHeaderToken: false }),
        'Csrf',
      );
    });

    it('rejects with 403 when the header value mismatches the session token', () => {
      const csrf = createCsrfMiddleware(makeLogger());
      const req = makeReq({
        method: 'POST',
        path: '/api/profile',
        headers: { 'X-CSRF-Token': 'wrong-token' },
        session: { user: { oid: 'u' }, csrfToken: 'expected-token' },
      });
      const res = makeRes();
      csrf(req, res, next);
      expect(res.status).toHaveBeenCalledWith(403);
    });

    it('rejects with 403 when the session has no csrfToken (auth without CSRF — should never happen)', () => {
      const csrf = createCsrfMiddleware(makeLogger());
      const req = makeReq({
        method: 'POST',
        path: '/api/profile',
        headers: { 'X-CSRF-Token': 'anything' },
        session: { user: { oid: 'u' } },
      });
      const res = makeRes();
      csrf(req, res, next);
      expect(res.status).toHaveBeenCalledWith(403);
    });

    it('passes when header matches the session token exactly', () => {
      const csrf = createCsrfMiddleware(makeLogger());
      const req = makeReq({
        method: 'POST',
        path: '/api/profile',
        headers: { 'X-CSRF-Token': 'expected-token' },
        session: { user: { oid: 'u' }, csrfToken: 'expected-token' },
      });
      const res = makeRes();
      csrf(req, res, next);
      expect(next).toHaveBeenCalledTimes(1);
      expect(res.status).not.toHaveBeenCalled();
    });

    it.each(['PUT', 'PATCH', 'DELETE'])('enforces on mutation method %s like POST', (method) => {
      const csrf = createCsrfMiddleware(makeLogger());
      const req = makeReq({
        method,
        path: '/api/profile',
        headers: { 'X-CSRF-Token': 'tok' },
        session: { user: { oid: 'u' }, csrfToken: 'tok' },
      });
      const res = makeRes();
      csrf(req, res, next);
      expect(next).toHaveBeenCalledTimes(1);
    });

    it('uses constant-time comparison (different lengths short-circuit as unequal)', () => {
      const csrf = createCsrfMiddleware(makeLogger());
      const req = makeReq({
        method: 'POST',
        path: '/api/profile',
        headers: { 'X-CSRF-Token': 'short' },
        session: { user: { oid: 'u' }, csrfToken: 'much-longer-token-value' },
      });
      const res = makeRes();
      csrf(req, res, next);
      expect(res.status).toHaveBeenCalledWith(403);
    });
  });
});
