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

const repoMock = {
  listPolicies: vi.fn(),
  createPolicy: vi.fn(),
  getPolicy: vi.fn(),
  updatePolicy: vi.fn(),
  createSpendEntry: vi.fn(),
  listSpendEntries: vi.fn(),
  createAlerts: vi.fn(),
  listAlerts: vi.fn(),
  listAllSpendEntries: vi.fn(),
  createRollover: vi.fn(),
  listRollovers: vi.fn(),
};

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

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

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

  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('POST /ai-budgets/policies creates a policy', async () => {
    repoMock.createPolicy.mockResolvedValue({ id: 'aibudget_1' });

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({
      method: 'POST',
      url: '/api/ai-budgets/policies',
      payload: {
        scopeType: 'agent',
        scopeId: 'agt_1',
        name: 'Incident Budget',
        budgetUsd: 20,
      },
    });

    expect(res.statusCode).toBe(200);
    expect(repoMock.createPolicy).toHaveBeenCalledWith(
      expect.objectContaining({
        scopeType: 'agent',
        scopeId: 'agt_1',
        budgetUsd: 20,
      })
    );
  });

  it('GET /ai-budgets/policies/:id/status summarizes current spend', async () => {
    repoMock.getPolicy.mockResolvedValue({
      id: 'aibudget_1',
      productId: 'lysnrai',
      scopeType: 'agent',
      scopeId: 'agt_1',
      name: 'Incident Budget',
      period: 'monthly',
      budgetUsd: 10,
      softThreshold: 0.8,
      hardThreshold: 1,
    });
    repoMock.listSpendEntries.mockResolvedValue([
      { costUsd: 3, tokensUsed: 100, recordedAt: '2026-03-15T00:00:00.000Z' },
      { costUsd: 6, tokensUsed: 200, recordedAt: '2026-03-15T01:00:00.000Z' },
    ]);

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({
      method: 'GET',
      url: '/api/ai-budgets/policies/aibudget_1/status',
    });

    expect(res.statusCode).toBe(200);
    expect(res.json()).toMatchObject({
      spendUsd: 9,
      tokensUsed: 300,
      verdict: 'warn',
    });
  });

  it('POST /ai-budgets/spend blocks disallowed models and persists alerts', async () => {
    repoMock.getPolicy.mockResolvedValue({
      id: 'aibudget_1',
      productId: 'lysnrai',
      scopeType: 'agent',
      scopeId: 'agt_1',
      name: 'Incident Budget',
      status: 'active',
      period: 'monthly',
      budgetUsd: 50,
      softThreshold: 0.8,
      hardThreshold: 1,
      modelAllowlist: ['gpt-4o-mini'],
    });
    repoMock.createSpendEntry.mockResolvedValue({ id: 'aispend_1', verdict: 'block' });
    repoMock.createAlerts.mockResolvedValue([]);

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({
      method: 'POST',
      url: '/api/ai-budgets/spend',
      payload: {
        policyId: 'aibudget_1',
        scopeType: 'agent',
        scopeId: 'agt_1',
        model: 'gpt-4.1',
        costUsd: 2,
      },
    });

    expect(res.statusCode).toBe(200);
    expect(repoMock.createAlerts).toHaveBeenCalled();
    expect(repoMock.createSpendEntry).toHaveBeenCalledWith(
      expect.objectContaining({
        verdict: 'block',
        model: 'gpt-4.1',
      })
    );
  });

  // ── Scope expansion ─────────────────────────────────────

  it('POST /ai-budgets/policies supports org scope', async () => {
    repoMock.createPolicy.mockResolvedValue({ id: 'aibudget_2', scopeType: 'org' });
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({
      method: 'POST',
      url: '/api/ai-budgets/policies',
      payload: { scopeType: 'org', scopeId: 'org_1', name: 'Org Budget', budgetUsd: 500 },
    });

    expect(res.statusCode).toBe(200);
    expect(repoMock.createPolicy).toHaveBeenCalledWith(
      expect.objectContaining({ scopeType: 'org', scopeId: 'org_1' })
    );
  });

  it('POST /ai-budgets/policies supports workspace scope', async () => {
    repoMock.createPolicy.mockResolvedValue({ id: 'aibudget_3', scopeType: 'workspace' });
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({
      method: 'POST',
      url: '/api/ai-budgets/policies',
      payload: { scopeType: 'workspace', scopeId: 'ws_1', name: 'WS Budget', budgetUsd: 100 },
    });

    expect(res.statusCode).toBe(200);
    expect(repoMock.createPolicy).toHaveBeenCalledWith(
      expect.objectContaining({ scopeType: 'workspace' })
    );
  });

  // ── Cost Dashboard ──────────────────────────────────────

  it('GET /ai-budgets/costs returns grouped cost breakdown', async () => {
    repoMock.listAllSpendEntries.mockResolvedValue([
      { costUsd: 5, tokensUsed: 100, model: 'gpt-4o', recordedAt: '2026-03-15T00:00:00Z' },
      { costUsd: 3, tokensUsed: 80, model: 'gpt-4o', recordedAt: '2026-03-15T01:00:00Z' },
      { costUsd: 1, tokensUsed: 50, model: 'gpt-4o-mini', recordedAt: '2026-03-15T02:00:00Z' },
    ]);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'GET', url: '/api/ai-budgets/costs?groupBy=model' });
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.totalCostUsd).toBe(9);
    expect(body.totalTokens).toBe(230);
    expect(body.breakdown).toHaveLength(2);
    expect(body.breakdown[0].key).toBe('gpt-4o');
  });

  it('GET /ai-budgets/costs groups by day', async () => {
    repoMock.listAllSpendEntries.mockResolvedValue([
      { costUsd: 5, tokensUsed: 100, recordedAt: '2026-03-15T00:00:00Z' },
      { costUsd: 3, tokensUsed: 80, recordedAt: '2026-03-16T01:00:00Z' },
    ]);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'GET', url: '/api/ai-budgets/costs?groupBy=day' });
    expect(res.statusCode).toBe(200);
    expect(res.json().breakdown).toHaveLength(2);
  });

  // ── Budget Rollover ─────────────────────────────────────

  it('POST /ai-budgets/policies/:id/rollover creates rollover and adjusts budget', async () => {
    repoMock.getPolicy.mockResolvedValue({
      id: 'aibudget_1',
      productId: 'lysnrai',
      period: 'monthly',
      budgetUsd: 100,
      metadata: {},
    });
    repoMock.listSpendEntries.mockResolvedValue([
      { costUsd: 60, recordedAt: '2026-02-10T00:00:00Z' },
    ]);
    repoMock.createRollover.mockImplementation(async (doc: Record<string, unknown>) => doc);
    repoMock.updatePolicy.mockResolvedValue({ budgetUsd: 140 });

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({
      method: 'POST',
      url: '/api/ai-budgets/policies/aibudget_1/rollover',
    });

    expect(res.statusCode).toBe(200);
    expect(repoMock.createRollover).toHaveBeenCalled();
    expect(repoMock.updatePolicy).toHaveBeenCalledWith(
      'aibudget_1',
      'lysnrai',
      expect.objectContaining({ budgetUsd: expect.any(Number) })
    );
  });

  it('GET /ai-budgets/policies/:id/rollovers returns rollover history', async () => {
    repoMock.listRollovers.mockResolvedValue([
      { id: 'airollover_1', policyId: 'aibudget_1', rolledOverUsd: 40 },
    ]);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({
      method: 'GET',
      url: '/api/ai-budgets/policies/aibudget_1/rollovers',
    });
    expect(res.statusCode).toBe(200);
    expect(res.json()).toHaveLength(1);
  });

  // ── Enforcement Check ───────────────────────────────────

  it('POST /ai-budgets/check returns allow when under budget', async () => {
    repoMock.listPolicies.mockResolvedValue([
      {
        id: 'aibudget_1',
        status: 'active',
        period: 'monthly',
        budgetUsd: 100,
        softThreshold: 0.8,
        hardThreshold: 1,
        modelAllowlist: [],
        name: 'Test',
      },
    ]);
    repoMock.listSpendEntries.mockResolvedValue([
      { costUsd: 20, recordedAt: '2026-03-15T00:00:00Z' },
    ]);

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({
      method: 'POST',
      url: '/api/ai-budgets/check',
      payload: { scopeType: 'agent', scopeId: 'agt_1', estimatedCostUsd: 5 },
    });

    expect(res.statusCode).toBe(200);
    expect(res.json().verdict).toBe('allow');
  });

  it('POST /ai-budgets/check returns block when model not allowed', async () => {
    repoMock.listPolicies.mockResolvedValue([
      {
        id: 'aibudget_1',
        status: 'active',
        period: 'monthly',
        budgetUsd: 100,
        softThreshold: 0.8,
        hardThreshold: 1,
        modelAllowlist: ['gpt-4o-mini'],
        name: 'Test',
      },
    ]);

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({
      method: 'POST',
      url: '/api/ai-budgets/check',
      payload: { scopeType: 'agent', scopeId: 'agt_1', estimatedCostUsd: 5, model: 'gpt-4.1' },
    });

    expect(res.statusCode).toBe(200);
    expect(res.json().verdict).toBe('block');
    expect(res.json().reasons[0]).toContain('not in allowlist');
  });

  it('POST /ai-budgets/check returns warn when approaching soft threshold', async () => {
    repoMock.listPolicies.mockResolvedValue([
      {
        id: 'aibudget_1',
        status: 'active',
        period: 'monthly',
        budgetUsd: 100,
        softThreshold: 0.8,
        hardThreshold: 1,
        modelAllowlist: [],
        name: 'Test',
      },
    ]);
    repoMock.listSpendEntries.mockResolvedValue([
      { costUsd: 75, recordedAt: '2026-03-15T00:00:00Z' },
    ]);

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({
      method: 'POST',
      url: '/api/ai-budgets/check',
      payload: { scopeType: 'agent', scopeId: 'agt_1', estimatedCostUsd: 10 },
    });

    expect(res.statusCode).toBe(200);
    expect(res.json().verdict).toBe('warn');
  });
});
