import { describe, it, expect, vi, beforeEach } from 'vitest';
import Fastify from 'fastify';
import { fastifyAwilixPlugin } from '@fastify/awilix';
import { asValue } from 'awilix';

const { MockJOSEError } = vi.hoisted(() => ({
  MockJOSEError: class JOSEError extends Error {},
}));

// Mock jose before importing the plugin
vi.mock('jose', () => ({
  createRemoteJWKSet: vi.fn(() => 'mock-jwks'),
  jwtVerify: vi.fn(),
  errors: { JOSEError: MockJOSEError },
}));

const { mockFindByKeycloakId, mockUpsertFromToken } = vi.hoisted(() => ({
  mockFindByKeycloakId: vi.fn(),
  mockUpsertFromToken: vi.fn(),
}));

import authPlugin from '../../src/plugins/auth.plugin.js';
import * as jose from 'jose';

describe('auth.plugin', () => {
  async function buildApp() {
    const app = Fastify({ logger: false });
    await app.register(fastifyAwilixPlugin, {
      disposeOnClose: true,
      disposeOnResponse: true,
      strictBooleanEnforced: true,
    });
    app.diContainer.register({
      usersRepository: asValue({
        findByKeycloakId: mockFindByKeycloakId,
        upsertFromToken: mockUpsertFromToken,
      }),
    });
    return app;
  }

  beforeEach(() => {
    vi.clearAllMocks();
    mockFindByKeycloakId.mockResolvedValue({ householdIds: ['hh1'] });
  });

  it('skips auth for routes marked as public', async () => {
    const app = await buildApp();
    await app.register(authPlugin);

    app.get('/public', { config: { public: true } as never }, async () => ({ ok: true }));
    await app.ready();

    const res = await app.inject({ method: 'GET', url: '/public' });
    expect(res.statusCode).toBe(200);
  });

  it('throws 401 when no Authorization header', async () => {
    const app = await buildApp();
    await app.register(authPlugin);

    app.get('/protected', async () => ({ ok: true }));
    await app.ready();

    const res = await app.inject({ method: 'GET', url: '/protected' });
    expect(res.statusCode).toBe(401);
    expect(res.json().message).toContain('Missing or invalid Authorization');
  });

  it('throws 401 when Authorization header is not Bearer', async () => {
    const app = await buildApp();
    await app.register(authPlugin);

    app.get('/protected', async () => ({ ok: true }));
    await app.ready();

    const res = await app.inject({
      method: 'GET',
      url: '/protected',
      headers: { authorization: 'Basic abc123' },
    });
    expect(res.statusCode).toBe(401);
  });

  it('throws 401 when token is invalid', async () => {
    vi.mocked(jose.jwtVerify).mockRejectedValue(new MockJOSEError('Invalid token'));

    const app = await buildApp();
    await app.register(authPlugin);

    app.get('/protected', async () => ({ ok: true }));
    await app.ready();

    const res = await app.inject({
      method: 'GET',
      url: '/protected',
      headers: { authorization: 'Bearer invalid-token' },
    });
    expect(res.statusCode).toBe(401);
    expect(res.json().message).toContain('Invalid or expired token');
  });

  it('sets request.user from valid JWT payload', async () => {
    vi.mocked(jose.jwtVerify).mockResolvedValue({
      payload: {
        sub: 'kc-1',
        email: 'test@example.com',
        preferred_username: 'testuser',
        realm_access: { roles: ['member'] },
        iss: 'http://localhost:8080/realms/meshitrack',
        aud: 'meshitrack-api',
      },
      protectedHeader: { alg: 'RS256' },
      key: {} as never,
    } as never);
    mockFindByKeycloakId.mockResolvedValue({ householdIds: ['hh1'] });

    const app = await buildApp();
    await app.register(authPlugin);

    let capturedUser: unknown;
    app.get('/protected', async (request) => {
      capturedUser = request.user;
      return { ok: true };
    });
    await app.ready();

    const res = await app.inject({
      method: 'GET',
      url: '/protected',
      headers: { authorization: 'Bearer valid-token' },
    });

    expect(res.statusCode).toBe(200);
    expect(capturedUser).toEqual({
      keycloakId: 'kc-1',
      email: 'test@example.com',
      displayName: 'testuser',
      roles: ['member'],
      householdIds: ['hh1'],
    });
  });

  it('rethrows non-JOSE errors as-is', async () => {
    vi.mocked(jose.jwtVerify).mockRejectedValue(new Error('Network failure'));

    const app = await buildApp();
    await app.register(authPlugin);

    app.get('/protected', async () => ({ ok: true }));
    await app.ready();

    const res = await app.inject({
      method: 'GET',
      url: '/protected',
      headers: { authorization: 'Bearer some-token' },
    });
    expect(res.statusCode).toBe(500);
  });

  it('handles missing optional fields in JWT payload', async () => {
    vi.mocked(jose.jwtVerify).mockResolvedValue({
      payload: {
        // sub, email, preferred_username, realm_access all missing
        iss: 'http://localhost:8080/realms/meshitrack',
        aud: 'meshitrack-api',
      },
      protectedHeader: { alg: 'RS256' },
      key: {} as never,
    } as never);
    mockFindByKeycloakId.mockResolvedValue(null);
    mockUpsertFromToken.mockResolvedValue({ householdIds: [] });

    const app = await buildApp();
    await app.register(authPlugin);

    let capturedUser: unknown;
    app.get('/protected', async (request) => {
      capturedUser = request.user;
      return { ok: true };
    });
    await app.ready();

    const res = await app.inject({
      method: 'GET',
      url: '/protected',
      headers: { authorization: 'Bearer token' },
    });

    expect(res.statusCode).toBe(200);
    expect(capturedUser).toEqual({
      keycloakId: '',
      email: '',
      displayName: '',
      roles: [],
      householdIds: [],
    });
  });
});
