import { describe, it, expect, vi, beforeEach } from 'vitest';

// Mock the api-tokens model
vi.mock('@/src/lib/models/api-tokens', () => ({
  validateToken: vi.fn(),
}));

// Mock next-auth
vi.mock('@/src/lib/auth', () => ({
  auth: vi.fn(),
  checkSameOrigin: vi.fn(() => null),
}));

import { authenticateApiRequest, requireApiUser, requireApiAdmin, ApiAuthError, apiErrorResponse } from '@/src/lib/api-auth';
import { validateToken } from '@/src/lib/models/api-tokens';
import { auth, checkSameOrigin } from '@/src/lib/auth';
import { NextResponse } from 'next/server';

const mockValidateToken = vi.mocked(validateToken);
const mockAuth = vi.mocked(auth);

function createMockRequest(options: { authorization?: string; method?: string; origin?: string } = {}): any {
  return {
    headers: {
      get(name: string) {
        if (name === 'authorization') return options.authorization ?? null;
        if (name === 'origin') return options.origin ?? null;
        return null;
      },
    },
    method: options.method ?? 'GET',
    nextUrl: { pathname: '/api/v1/test' },
  };
}

beforeEach(() => {
  vi.resetAllMocks();
});

describe('authenticateApiRequest', () => {
  it('authenticates via Bearer token', async () => {
    mockValidateToken.mockResolvedValue({
      token: { id: 1, name: 'test', createdBy: 42, createdAt: '', lastUsedAt: null, expiresAt: null },
      user: { id: 42, role: 'admin' },
    });

    const result = await authenticateApiRequest(createMockRequest({ authorization: 'Bearer test-token' }));

    expect(result.userId).toBe(42);
    expect(result.role).toBe('admin');
    expect(result.authMethod).toBe('bearer');
    expect(mockValidateToken).toHaveBeenCalledWith('test-token');
  });

  it('rejects invalid Bearer token', async () => {
    mockValidateToken.mockResolvedValue(null);

    await expect(
      authenticateApiRequest(createMockRequest({ authorization: 'Bearer bad-token' }))
    ).rejects.toThrow(ApiAuthError);
  });

  it('falls back to session auth when no Bearer header', async () => {
    mockAuth.mockResolvedValue({
      user: { id: '10', role: 'user', name: 'Test', email: 'test@test.com' },
      expires: '',
    } as any);

    const result = await authenticateApiRequest(createMockRequest());

    expect(result.userId).toBe(10);
    expect(result.role).toBe('user');
    expect(result.authMethod).toBe('session');
  });

  it('throws 401 when neither auth method succeeds', async () => {
    mockAuth.mockResolvedValue(null as any);

    await expect(
      authenticateApiRequest(createMockRequest())
    ).rejects.toThrow(ApiAuthError);

    try {
      await authenticateApiRequest(createMockRequest());
    } catch (e) {
      expect((e as ApiAuthError).status).toBe(401);
    }
  });
});

describe('requireApiAdmin', () => {
  it('allows admin users', async () => {
    mockValidateToken.mockResolvedValue({
      token: { id: 1, name: 'test', createdBy: 1, createdAt: '', lastUsedAt: null, expiresAt: null },
      user: { id: 1, role: 'admin' },
    });

    const result = await requireApiAdmin(createMockRequest({ authorization: 'Bearer token' }));
    expect(result.role).toBe('admin');
  });

  it('rejects non-admin users with 403', async () => {
    mockValidateToken.mockResolvedValue({
      token: { id: 1, name: 'test', createdBy: 2, createdAt: '', lastUsedAt: null, expiresAt: null },
      user: { id: 2, role: 'user' },
    });

    try {
      await requireApiAdmin(createMockRequest({ authorization: 'Bearer token' }));
      expect.fail('Should have thrown');
    } catch (e) {
      expect(e).toBeInstanceOf(ApiAuthError);
      expect((e as ApiAuthError).status).toBe(403);
    }
  });
});

describe('requireApiUser', () => {
  it('returns auth result for valid user', async () => {
    mockAuth.mockResolvedValue({
      user: { id: '5', role: 'viewer', name: 'V', email: 'v@test.com' },
      expires: '',
    } as any);

    const result = await requireApiUser(createMockRequest());
    expect(result.userId).toBe(5);
    expect(result.role).toBe('viewer');
  });

  it('CSRF check blocks session-authenticated POST without same origin', async () => {
    mockAuth.mockResolvedValue({
      user: { id: '5', role: 'user', name: 'U', email: 'u@test.com' },
      expires: '',
    } as any);

    const mockCheckSameOrigin = vi.mocked(checkSameOrigin);
    mockCheckSameOrigin.mockReturnValueOnce(NextResponse.json({ error: 'Forbidden' }, { status: 403 }) as any);

    await expect(
      requireApiUser(createMockRequest({ method: 'POST' }))
    ).rejects.toThrow(ApiAuthError);

    try {
      mockCheckSameOrigin.mockReturnValueOnce(NextResponse.json({ error: 'Forbidden' }, { status: 403 }) as any);
      mockAuth.mockResolvedValue({
        user: { id: '5', role: 'user', name: 'U', email: 'u@test.com' },
        expires: '',
      } as any);
      await requireApiUser(createMockRequest({ method: 'POST' }));
    } catch (e) {
      expect((e as ApiAuthError).status).toBe(403);
    }
  });

  it('CSRF check skips for Bearer-authenticated POST', async () => {
    mockValidateToken.mockResolvedValue({
      token: { id: 1, name: 'test', createdBy: 42, createdAt: '', lastUsedAt: null, expiresAt: null },
      user: { id: 42, role: 'admin' },
    });

    const mockCheckSameOrigin = vi.mocked(checkSameOrigin);
    mockCheckSameOrigin.mockReturnValueOnce(NextResponse.json({ error: 'Forbidden' }, { status: 403 }) as any);

    const result = await requireApiUser(createMockRequest({ authorization: 'Bearer test-token', method: 'POST' }));
    expect(result.userId).toBe(42);
    expect(result.authMethod).toBe('bearer');
  });
});

describe('authenticateApiRequest - empty bearer', () => {
  it('rejects empty Bearer token', async () => {
    await expect(
      authenticateApiRequest(createMockRequest({ authorization: 'Bearer ' }))
    ).rejects.toThrow(ApiAuthError);

    try {
      await authenticateApiRequest(createMockRequest({ authorization: 'Bearer ' }));
    } catch (e) {
      expect((e as ApiAuthError).status).toBe(401);
      expect((e as ApiAuthError).message).toBe('Invalid Bearer token');
    }
  });
});

describe('apiErrorResponse', () => {
  it('handles ApiAuthError', async () => {
    const response = apiErrorResponse(new ApiAuthError('Forbidden', 403));
    expect(response.status).toBe(403);
    const data = await response.json();
    expect(data.error).toBe('Forbidden');
  });

  it('handles generic Error', async () => {
    const response = apiErrorResponse(new Error('Something broke'));
    expect(response.status).toBe(500);
    const data = await response.json();
    expect(data.error).toBe('Something broke');
  });

  it('handles unknown error', async () => {
    const response = apiErrorResponse('some string error');
    expect(response.status).toBe(500);
    const data = await response.json();
    expect(data.error).toBe('Internal server error');
  });
});
