/**
 * Route-level tests for tokens module — Fastify inject.
 */

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

const repoMock = {
  list: vi.fn(),
  listByUser: vi.fn(),
  hashToken: vi.fn(),
  create: vi.fn(),
  countActive: vi.fn(),
  revoke: vi.fn(),
  remove: vi.fn(),
};

vi.mock('./repository.js', () => repoMock);

const baseToken = {
  id: 'tok_1',
  productId: 'lysnrai',
  userId: 'admin_1',
  userName: 'admin@example.com',
  name: 'ci-token',
  tokenType: 'service_api',
  environment: 'prod',
  prefix: 'wai_12345678',
  status: 'active',
  scopes: ['read'],
  createdAt: '2026-02-16T00:00:00Z',
  expiresAt: '2026-05-16T00:00:00Z',
  lastUsed: null,
};

async function buildApp(payload?: {
  sub: string;
  productId: string;
  role?: string;
  email?: string;
}) {
  const { tokenRoutes } = await import('./routes.js');
  const app = Fastify({ logger: false });
  if (payload) {
    app.addHook('onRequest', async req => {
      (req as typeof req & { jwtPayload?: typeof payload }).jwtPayload = payload;
    });
  }
  await app.register(tokenRoutes, { prefix: '/api' });
  return app;
}

describe('tokenRoutes', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  afterEach(() => {
    vi.restoreAllMocks();
    delete process.env.PLATFORM_RUNTIME_ENV;
  });

  it('GET /tokens returns 401 when unauthenticated', async () => {
    const app = await buildApp();
    const res = await app.inject({ method: 'GET', url: '/api/tokens' });
    expect(res.statusCode).toBe(401);
  });

  it('GET /tokens lists all tokens for admin', async () => {
    repoMock.list.mockResolvedValue([baseToken]);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'GET', url: '/api/tokens' });

    expect(res.statusCode).toBe(200);
    const data = JSON.parse(res.body);
    expect(data.tokens).toHaveLength(1);
    expect(repoMock.list).toHaveBeenCalledWith('lysnrai');
  });

  it('GET /tokens lists own tokens for non-admin user', async () => {
    repoMock.listByUser.mockResolvedValue([baseToken]);
    const app = await buildApp({ sub: 'user_1', productId: 'lysnrai', role: 'user' });

    const res = await app.inject({ method: 'GET', url: '/api/tokens' });

    expect(res.statusCode).toBe(200);
    expect(repoMock.listByUser).toHaveBeenCalledWith('user_1', 'lysnrai');
  });

  it('POST /tokens returns 403 for non-admin', async () => {
    const app = await buildApp({ sub: 'user_1', productId: 'lysnrai', role: 'user' });

    const res = await app.inject({
      method: 'POST',
      url: '/api/tokens',
      payload: { name: 'token', scopes: ['read'], expiresInDays: 30 },
    });

    expect(res.statusCode).toBe(403);
  });

  it('POST /tokens creates token for admin', async () => {
    repoMock.hashToken.mockResolvedValue('hashed_token');
    repoMock.create.mockResolvedValue(baseToken);
    const app = await buildApp({
      sub: 'admin_1',
      productId: 'lysnrai',
      role: 'admin',
      email: 'admin@example.com',
    });

    const res = await app.inject({
      method: 'POST',
      url: '/api/tokens',
      payload: { name: 'ci-token', scopes: ['read'], expiresInDays: 30 },
    });

    expect(res.statusCode).toBe(201);
    const data = JSON.parse(res.body);
    expect(data).toHaveProperty('rawToken');
    expect(data.rawToken.startsWith('wai_')).toBe(true);
    expect(repoMock.create).toHaveBeenCalledWith(
      expect.objectContaining({
        tokenType: 'product_api',
        environment: 'dev',
      })
    );
    expect(repoMock.hashToken).toHaveBeenCalled();
  });

  it('POST /tokens rejects unsupported user_api tokens', async () => {
    const app = await buildApp({
      sub: 'admin_1',
      productId: 'lysnrai',
      role: 'admin',
      email: 'admin@example.com',
    });

    const res = await app.inject({
      method: 'POST',
      url: '/api/tokens',
      payload: {
        name: 'legacy-user-token',
        tokenType: 'user_api',
        scopes: ['read'],
        expiresInDays: 30,
      },
    });

    expect(res.statusCode).toBe(400);
    expect(repoMock.create).not.toHaveBeenCalled();
  });

  it('POST /tokens rejects service_api creation for non-super-admins', async () => {
    const app = await buildApp({
      sub: 'admin_1',
      productId: 'lysnrai',
      role: 'admin',
      email: 'admin@example.com',
    });

    const res = await app.inject({
      method: 'POST',
      url: '/api/tokens',
      payload: {
        name: 'service-token',
        tokenType: 'service_api',
        scopes: ['jobs:read'],
        expiresInDays: 30,
      },
    });

    expect(res.statusCode).toBe(403);
    expect(repoMock.create).not.toHaveBeenCalled();
  });

  it('POST /tokens allows service_api creation for super_admin', async () => {
    repoMock.hashToken.mockResolvedValue('hashed_token');
    repoMock.create.mockResolvedValue(baseToken);
    const app = await buildApp({
      sub: 'root_1',
      productId: 'lysnrai',
      role: 'super_admin',
      email: 'root@example.com',
    });

    const res = await app.inject({
      method: 'POST',
      url: '/api/tokens',
      payload: {
        name: 'service-token',
        tokenType: 'service_api',
        scopes: ['jobs:read'],
        expiresInDays: 30,
      },
    });

    expect(res.statusCode).toBe(201);
    expect(repoMock.create).toHaveBeenCalledWith(
      expect.objectContaining({
        tokenType: 'service_api',
      })
    );
  });

  it('POST /tokens respects explicit environment overrides', async () => {
    repoMock.hashToken.mockResolvedValue('hashed_token');
    repoMock.create.mockResolvedValue(baseToken);
    const app = await buildApp({
      sub: 'root_1',
      productId: 'lysnrai',
      role: 'super_admin',
      email: 'root@example.com',
    });

    const res = await app.inject({
      method: 'POST',
      url: '/api/tokens',
      payload: {
        name: 'staging-token',
        tokenType: 'service_api',
        environment: 'staging',
        scopes: ['jobs:read'],
        expiresInDays: 30,
      },
    });

    expect(res.statusCode).toBe(201);
    expect(repoMock.create).toHaveBeenCalledWith(
      expect.objectContaining({
        tokenType: 'service_api',
        environment: 'staging',
      })
    );
  });

  it('POST /tokens defaults environment from runtime configuration', async () => {
    process.env.PLATFORM_RUNTIME_ENV = 'staging';
    repoMock.hashToken.mockResolvedValue('hashed_token');
    repoMock.create.mockResolvedValue(baseToken);
    const app = await buildApp({
      sub: 'admin_1',
      productId: 'lysnrai',
      role: 'admin',
      email: 'admin@example.com',
    });

    const res = await app.inject({
      method: 'POST',
      url: '/api/tokens',
      payload: { name: 'runtime-default-token', scopes: ['jobs:read'], expiresInDays: 30 },
    });

    expect(res.statusCode).toBe(201);
    expect(repoMock.create).toHaveBeenCalledWith(
      expect.objectContaining({
        tokenType: 'product_api',
        environment: 'staging',
      })
    );
  });

  it('GET /tokens/count returns count for admin', async () => {
    repoMock.countActive.mockResolvedValue(4);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'GET', url: '/api/tokens/count' });

    expect(res.statusCode).toBe(200);
    const data = JSON.parse(res.body);
    expect(data.count).toBe(4);
  });

  it('PATCH /tokens/:id revokes token', async () => {
    repoMock.revoke.mockResolvedValue(true);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({
      method: 'PATCH',
      url: '/api/tokens/tok_1',
      payload: { action: 'revoke' },
    });

    expect(res.statusCode).toBe(200);
    const data = JSON.parse(res.body);
    expect(data.success).toBe(true);
  });

  it('PATCH /tokens/:id returns 400 when token not found', async () => {
    repoMock.revoke.mockResolvedValue(false);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({
      method: 'PATCH',
      url: '/api/tokens/tok_missing',
      payload: { action: 'revoke' },
    });

    expect(res.statusCode).toBe(400);
  });

  it('DELETE /tokens/:id returns 403 for non-super-admin', async () => {
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'DELETE', url: '/api/tokens/tok_1' });

    expect(res.statusCode).toBe(403);
  });

  it('DELETE /tokens/:id deletes token for super_admin', async () => {
    repoMock.remove.mockResolvedValue(true);
    const app = await buildApp({ sub: 'root_1', productId: 'lysnrai', role: 'super_admin' });

    const res = await app.inject({ method: 'DELETE', url: '/api/tokens/tok_1' });

    expect(res.statusCode).toBe(200);
    const data = JSON.parse(res.body);
    expect(data.success).toBe(true);
    expect(repoMock.remove).toHaveBeenCalledWith('tok_1');
  });
});
