import { describe, it, expect } from 'vitest';
import { SignJWT } from 'jose';
import { createAuthMiddleware, createRequestContext } from './index.js';
import type { JwtPayload } from './types.js';

const TEST_SECRET = 'test-jwt-secret-for-fastify-auth-package';

interface MockReq {
  headers: Record<string, string | undefined>;
  jwtPayload?: JwtPayload;
}

function makeReq(token?: string): MockReq {
  return {
    headers: {
      authorization: token ? `Bearer ${token}` : undefined,
    },
    jwtPayload: undefined,
  };
}

async function signToken(payload: Record<string, unknown>, secret = TEST_SECRET) {
  return new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('5m')
    .sign(new TextEncoder().encode(secret));
}

describe('createAuthMiddleware', () => {
  const { extractAuth, requireRole } = createAuthMiddleware({
    jwtSecret: TEST_SECRET,
  });

  it('extracts auth payload from valid access token', async () => {
    const token = await signToken({
      sub: 'user-1',
      email: 'test@test.com',
      role: 'admin',
      type: 'access',
    });
    const payload = await extractAuth(makeReq(token));
    expect(payload.sub).toBe('user-1');
    expect(payload.email).toBe('test@test.com');
    expect(payload.role).toBe('admin');
  });

  it('throws UnauthorizedError when no Authorization header', async () => {
    await expect(extractAuth(makeReq())).rejects.toThrow('Unauthorized');
  });

  it('throws UnauthorizedError for invalid token', async () => {
    await expect(extractAuth(makeReq('bad-token'))).rejects.toThrow('Invalid or expired token');
  });

  it('throws UnauthorizedError for non-access token type', async () => {
    const token = await signToken({ sub: 'u1', type: 'refresh' });
    await expect(extractAuth(makeReq(token))).rejects.toThrow('Invalid or expired token');
  });

  it('throws UnauthorizedError for wrong secret', async () => {
    const token = await signToken({ sub: 'u1', type: 'access' }, 'wrong');
    await expect(extractAuth(makeReq(token))).rejects.toThrow('Invalid or expired token');
  });

  it('requireRole passes when role matches', async () => {
    const token = await signToken({
      sub: 'u1',
      role: 'admin',
      type: 'access',
    });
    const payload = await requireRole(makeReq(token), 'admin', 'superadmin');
    expect(payload.sub).toBe('u1');
  });

  it('requireRole throws ForbiddenError when role does not match', async () => {
    const token = await signToken({
      sub: 'u1',
      role: 'viewer',
      type: 'access',
    });
    await expect(requireRole(makeReq(token), 'admin')).rejects.toThrow('Insufficient permissions');
  });

  it('requireRole passes with no required roles (any authenticated user)', async () => {
    const token = await signToken({
      sub: 'u1',
      type: 'access',
    });
    const payload = await requireRole(makeReq(token));
    expect(payload.sub).toBe('u1');
  });
});

describe('createRequestContext', () => {
  const { getRequestProductId, getUserId } = createRequestContext({
    productId: 'testproduct',
  });

  function makeFastifyReq(overrides?: {
    jwtPayload?: JwtPayload;
    headers?: Record<string, string | undefined>;
  }) {
    const req = makeReq();
    if (overrides?.jwtPayload !== undefined) req.jwtPayload = overrides.jwtPayload;
    if (overrides?.headers) Object.assign(req.headers, overrides.headers);
    return req as unknown as import('fastify').FastifyRequest;
  }

  it('returns product ID for valid request', () => {
    expect(getRequestProductId(makeFastifyReq())).toBe('testproduct');
  });

  it('returns product ID when JWT productId matches', () => {
    expect(
      getRequestProductId(makeFastifyReq({ jwtPayload: { sub: 'u1', productId: 'testproduct' } }))
    ).toBe('testproduct');
  });

  it('throws BadRequestError when JWT productId does not match', () => {
    expect(() =>
      getRequestProductId(makeFastifyReq({ jwtPayload: { sub: 'u1', productId: 'wrong' } }))
    ).toThrow('Invalid productId');
  });

  it('throws BadRequestError when X-Product-Id header does not match', () => {
    expect(() =>
      getRequestProductId(makeFastifyReq({ headers: { 'x-product-id': 'wrong' } }))
    ).toThrow('Invalid productId');
  });

  it('getUserId returns sub from JWT payload', () => {
    expect(getUserId(makeFastifyReq({ jwtPayload: { sub: 'user-42' } }))).toBe('user-42');
  });

  it('getUserId throws when no JWT payload', () => {
    expect(() => getUserId(makeFastifyReq())).toThrow('Missing userId');
  });

  it('getUserId throws when JWT has no sub', () => {
    expect(() => getUserId(makeFastifyReq({ jwtPayload: {} as JwtPayload }))).toThrow(
      'Missing userId'
    );
  });
});
