import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { createHmac } from 'crypto';
import type { Request, Response, NextFunction } from 'express';

// SHARED_SECRET is read at module load time as a const.
// To control it per test we reset modules and set env before re-importing.

type HmacMiddlewareFn = (req: Request, res: Response, next: NextFunction) => void;

function makeReqResNext(overrides: {
  headers?: Record<string, string>;
  body?: unknown;
}) {
  const req = {
    headers: overrides.headers ?? {},
    body: overrides.body ?? {},
  } as unknown as Request;

  const json = vi.fn();
  const status = vi.fn().mockReturnValue({ json });
  const res = { status, json } as unknown as Response;
  const next = vi.fn() as unknown as NextFunction;

  return { req, res, next, json, status };
}

function validTimestamp(): number {
  return Math.floor(Date.now() / 1000);
}

function makeSignature(secret: string, timestamp: number, body: unknown): string {
  const payload = JSON.stringify(body);
  return createHmac('sha256', secret).update(`${timestamp}.${payload}`).digest('hex');
}

async function loadHmacWithSecret(secret: string): Promise<HmacMiddlewareFn> {
  vi.resetModules();
  process.env.NOTETOQUOTE_SHARED_SECRET = secret;
  const mod = await import('../lib/hmac.js');
  return mod.hmacMiddleware;
}

async function loadHmacWithoutSecret(): Promise<HmacMiddlewareFn> {
  vi.resetModules();
  delete process.env.NOTETOQUOTE_SHARED_SECRET;
  const mod = await import('../lib/hmac.js');
  return mod.hmacMiddleware;
}

describe('hmacMiddleware', () => {
  const SECRET = 'test-secret-value';

  afterEach(() => {
    vi.resetModules();
    delete process.env.NOTETOQUOTE_SHARED_SECRET;
  });

  it('returns 401 missing_signature when neither header is present', async () => {
    const hmacMiddleware = await loadHmacWithSecret(SECRET);
    const { req, res, next, status, json } = makeReqResNext({});
    hmacMiddleware(req, res, next);
    expect(status).toHaveBeenCalledWith(401);
    expect(json).toHaveBeenCalledWith({ error: 'missing_signature' });
    expect(next).not.toHaveBeenCalled();
  });

  it('returns 401 missing_signature when only timestamp header present', async () => {
    const hmacMiddleware = await loadHmacWithSecret(SECRET);
    const { req, res, next, status, json } = makeReqResNext({
      headers: { 'x-ntq-timestamp': String(validTimestamp()) },
    });
    hmacMiddleware(req, res, next);
    expect(status).toHaveBeenCalledWith(401);
    expect(json).toHaveBeenCalledWith({ error: 'missing_signature' });
    expect(next).not.toHaveBeenCalled();
  });

  it('returns 401 missing_signature when only signature header present', async () => {
    const hmacMiddleware = await loadHmacWithSecret(SECRET);
    const { req, res, next, status, json } = makeReqResNext({
      headers: { 'x-ntq-signature': 'abc123' },
    });
    hmacMiddleware(req, res, next);
    expect(status).toHaveBeenCalledWith(401);
    expect(json).toHaveBeenCalledWith({ error: 'missing_signature' });
    expect(next).not.toHaveBeenCalled();
  });

  it('returns 401 invalid_signature when signature is wrong', async () => {
    const hmacMiddleware = await loadHmacWithSecret(SECRET);
    const ts = validTimestamp();
    const body = { test: 'data' };
    const { req, res, next, status, json } = makeReqResNext({
      headers: {
        'x-ntq-timestamp': String(ts),
        'x-ntq-signature': 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
      },
      body,
    });
    hmacMiddleware(req, res, next);
    expect(status).toHaveBeenCalledWith(401);
    expect(json).toHaveBeenCalledWith({ error: 'invalid_signature' });
    expect(next).not.toHaveBeenCalled();
  });

  it('returns 401 request_expired when timestamp is > 5 min in the past', async () => {
    const hmacMiddleware = await loadHmacWithSecret(SECRET);
    const oldTs = Math.floor(Date.now() / 1000) - 301;
    const body = {};
    const sig = makeSignature(SECRET, oldTs, body);
    const { req, res, next, status, json } = makeReqResNext({
      headers: {
        'x-ntq-timestamp': String(oldTs),
        'x-ntq-signature': sig,
      },
      body,
    });
    hmacMiddleware(req, res, next);
    expect(status).toHaveBeenCalledWith(401);
    expect(json).toHaveBeenCalledWith({ error: 'request_expired' });
    expect(next).not.toHaveBeenCalled();
  });

  it('calls next() when signature is correct and timestamp is recent', async () => {
    const hmacMiddleware = await loadHmacWithSecret(SECRET);
    const ts = validTimestamp();
    const body = { licence_key: 'LAU-ABCD-1234-WXYZ' };
    const sig = makeSignature(SECRET, ts, body);
    const { req, res, next, status } = makeReqResNext({
      headers: {
        'x-ntq-timestamp': String(ts),
        'x-ntq-signature': sig,
      },
      body,
    });
    hmacMiddleware(req, res, next);
    expect(next).toHaveBeenCalled();
    expect(status).not.toHaveBeenCalled();
  });

  it('calls next() without verification when SHARED_SECRET is empty string', async () => {
    const hmacMiddleware = await loadHmacWithSecret('');
    const { req, res, next, status } = makeReqResNext({});
    hmacMiddleware(req, res, next);
    expect(next).toHaveBeenCalled();
    expect(status).not.toHaveBeenCalled();
  });

  it('calls next() without verification when SHARED_SECRET env var is not set', async () => {
    const hmacMiddleware = await loadHmacWithoutSecret();
    const { req, res, next, status } = makeReqResNext({});
    hmacMiddleware(req, res, next);
    expect(next).toHaveBeenCalled();
    expect(status).not.toHaveBeenCalled();
  });
});
