import { describe, it, expect, beforeEach } from 'vitest';
import { SignJWT } from 'jose';
import type { FastifyRequest } from 'fastify';

// Mock config so the auth module sees a deterministic JWT secret + product id.
// Mocks must be declared before importing the SUT.
vi.mock('./config.js', () => ({
  config: { JWT_SECRET: 'test-jwt-secret-for-unit-tests' },
  productId: 'devops-internal',
}));

const { extractAuth, requireAdmin, AuthError } = await import('./auth.js');

const SECRET = new TextEncoder().encode('test-jwt-secret-for-unit-tests');

async function makeToken(payload: Record<string, unknown>, opts?: { issuer?: string }): Promise<string> {
  return new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuer(opts?.issuer ?? 'bytelyst-platform')
    .setSubject((payload.sub as string) ?? 'user-1')
    .setExpirationTime('1h')
    .sign(SECRET);
}

function reqWith(headers: Record<string, string>): FastifyRequest {
  return { headers } as unknown as FastifyRequest;
}

describe('extractAuth', () => {
  it('returns null when Authorization header is missing', async () => {
    expect(await extractAuth(reqWith({}))).toBeNull();
  });

  it('returns null when Authorization is not a Bearer token', async () => {
    expect(await extractAuth(reqWith({ authorization: 'Basic abc' }))).toBeNull();
  });

  it('returns null when the token is malformed', async () => {
    expect(await extractAuth(reqWith({ authorization: 'Bearer not-a-jwt' }))).toBeNull();
  });

  it('returns null when issuer does not match', async () => {
    const token = await makeToken({ sub: 'u1', role: 'admin' }, { issuer: 'other-issuer' });
    expect(await extractAuth(reqWith({ authorization: `Bearer ${token}` }))).toBeNull();
  });

  it('returns null when signature does not verify (wrong secret)', async () => {
    const wrong = await new SignJWT({ sub: 'u1', role: 'admin' })
      .setProtectedHeader({ alg: 'HS256' })
      .setIssuer('bytelyst-platform')
      .setExpirationTime('1h')
      .sign(new TextEncoder().encode('different-secret'));
    expect(await extractAuth(reqWith({ authorization: `Bearer ${wrong}` }))).toBeNull();
  });

  it('elevates to admin when global role is admin', async () => {
    const token = await makeToken({ sub: 'u1', role: 'admin', email: 'a@b.com', productId: 'devops-internal' });
    const result = await extractAuth(reqWith({ authorization: `Bearer ${token}` }));
    expect(result).toEqual({
      userId: 'u1',
      role: 'admin',
      email: 'a@b.com',
      productId: 'devops-internal',
    });
  });

  it('elevates to admin via per-product membership for the target productId', async () => {
    const token = await makeToken({
      sub: 'u2',
      role: 'user',
      products: [
        { productId: 'other-product', role: 'admin', plan: 'pro' },
        { productId: 'devops-internal', role: 'admin', plan: 'pro' },
      ],
    });
    const result = await extractAuth(reqWith({ authorization: `Bearer ${token}` }));
    expect(result?.role).toBe('admin');
    expect(result?.userId).toBe('u2');
  });

  it('does not elevate when product membership is for a different product', async () => {
    const token = await makeToken({
      sub: 'u3',
      role: 'user',
      products: [{ productId: 'other-product', role: 'admin', plan: 'pro' }],
    });
    const result = await extractAuth(reqWith({ authorization: `Bearer ${token}` }));
    expect(result?.role).toBe('user');
  });

  it('does not elevate when product membership role is not admin', async () => {
    const token = await makeToken({
      sub: 'u4',
      role: 'user',
      products: [{ productId: 'devops-internal', role: 'viewer', plan: 'free' }],
    });
    const result = await extractAuth(reqWith({ authorization: `Bearer ${token}` }));
    expect(result?.role).toBe('user');
  });

  it('defaults role to "user" when payload has no role', async () => {
    const token = await makeToken({ sub: 'u5' });
    const result = await extractAuth(reqWith({ authorization: `Bearer ${token}` }));
    expect(result?.role).toBe('user');
  });
});

describe('requireAdmin', () => {
  let req: { authUserId?: string; authRole?: string };

  beforeEach(() => {
    req = {};
  });

  it('throws AuthError(403) when no auth context is present', () => {
    expect(() => requireAdmin(req as unknown as FastifyRequest)).toThrow(AuthError);
    try {
      requireAdmin(req as unknown as FastifyRequest);
    } catch (e) {
      expect((e as InstanceType<typeof AuthError>).statusCode).toBe(403);
    }
  });

  it('throws AuthError(403) for non-admin role', () => {
    req.authUserId = 'u1';
    req.authRole = 'user';
    expect(() => requireAdmin(req as unknown as FastifyRequest)).toThrow(AuthError);
  });

  it('returns userId when role is admin', () => {
    req.authUserId = 'u1';
    req.authRole = 'admin';
    expect(requireAdmin(req as unknown as FastifyRequest)).toEqual({ userId: 'u1' });
  });
});
