import { describe, it, expect, vi, beforeEach } from 'vitest';
import { POST } from '@/app/api/token/research/route';
import { NextRequest } from 'next/server';
import { db } from '@/lib/db';

// Mock database
vi.mock('@/lib/db', () => ({
  db: {
    tokenConfig: { findUnique: vi.fn(), create: vi.fn(), update: vi.fn() },
    tokenConversation: { create: vi.fn() },
    tokenMessage: { create: vi.fn(), findFirst: vi.fn() },
    user: { findUnique: vi.fn() },
    device: { findMany: vi.fn() },
  },
}));

// Mock auth
vi.mock('@/lib/auth', () => ({
  auth: vi.fn(),
}));

// Mock logger
vi.mock('@/lib/logger', () => ({
  logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn() },
}));

// Mock researcher agent
vi.mock('@/lib/token/agents/researcher', () => ({
  runResearch: vi.fn(),
}));

// Mock system prompt
vi.mock('@/lib/token/system-prompt', () => ({
  buildSystemPrompt: vi.fn().mockReturnValue('mock system prompt'),
}));

import { auth } from '@/lib/auth';
import { runResearch } from '@/lib/token/agents/researcher';

function createRequest(body: unknown): NextRequest {
  return new NextRequest('http://localhost:3000/api/token/research', {
    method: 'POST',
    body: JSON.stringify(body),
    headers: { 'Content-Type': 'application/json' },
  });
}

describe('POST /api/token/research', () => {
  beforeEach(() => {
    vi.clearAllMocks();

    // Default: authenticated user
    vi.mocked(auth).mockResolvedValue({
      user: { id: 'user_1', email: 'test@test.com' },
    } as any);

    // Default: config exists and enabled
    vi.mocked(db.tokenConfig.findUnique).mockResolvedValue({
      id: 'singleton',
      enabled: true,
      chatModel: 'google/gemini-2.5-flash',
      researchModel: 'anthropic/claude-sonnet-4.6',
      reasoningModel: 'deepseek/deepseek-r1',
      classificationModel: 'google/gemini-2.5-flash-lite',
      temperature: 0.4,
      maxTokensPerReply: 4096,
      monthlyBudgetUsd: 50,
      totalSpentUsd: 5,
      systemPrompt: '',
      updatedAt: new Date(),
    } as any);

    // Default: no cache hit
    vi.mocked(db.tokenMessage.findFirst).mockResolvedValue(null);

    // Default: user exists
    vi.mocked(db.user.findUnique).mockResolvedValue({
      name: 'Test User',
      plan: 'free',
    } as any);

    // Default: no devices
    vi.mocked(db.device.findMany).mockResolvedValue([]);

    // Default: conversation creation
    vi.mocked(db.tokenConversation.create).mockResolvedValue({
      id: 'conv_research',
      userId: 'user_1',
      title: 'Research: best model for 8GB',
    } as any);

    // Default: message creation
    vi.mocked(db.tokenMessage.create).mockResolvedValue({} as any);

    // Default: config update
    vi.mocked(db.tokenConfig.update).mockResolvedValue({} as any);

    // Default: research result
    vi.mocked(runResearch).mockResolvedValue({
      content: '## Summary\nQwen3:8b is recommended for 8GB VRAM.',
      model: 'anthropic/claude-sonnet-4.6',
      inputTokens: 200,
      outputTokens: 500,
      costUsd: 0.005,
      latencyMs: 3000,
    });
  });

  it('should return research result', async () => {
    // Act
    const response = await POST(createRequest({ query: 'best model for 8GB VRAM' }));
    const data = await response.json();

    // Assert
    expect(response.status).toBe(200);
    expect(data.result.content).toContain('Qwen3:8b is recommended');
    expect(data.result.model).toBe('anthropic/claude-sonnet-4.6');
    expect(data.cached).toBe(false);
    expect(data.conversationId).toBe('conv_research');

    // Verify research was called with correct params
    expect(runResearch).toHaveBeenCalledWith(
      expect.objectContaining({
        query: 'best model for 8GB VRAM',
        model: 'anthropic/claude-sonnet-4.6',
        temperature: 0.3,
        maxTokens: 8192,
      }),
    );
  });

  it('should return 401 without auth', async () => {
    // Arrange
    vi.mocked(auth).mockResolvedValue(null as any);

    // Act
    const response = await POST(createRequest({ query: 'test query' }));
    const data = await response.json();

    // Assert
    expect(response.status).toBe(401);
    expect(data.error).toBe('Unauthorized');
  });

  it('should return 400 without query', async () => {
    // Act
    const response = await POST(createRequest({}));
    const data = await response.json();

    // Assert
    expect(response.status).toBe(400);
    expect(data.error).toContain('query is required');
  });

  it('should return 400 for empty query', async () => {
    // Act
    const response = await POST(createRequest({ query: '   ' }));
    const data = await response.json();

    // Assert
    expect(response.status).toBe(400);
    expect(data.error).toContain('query is required');
  });

  it('should return 503 when Token is disabled', async () => {
    // Arrange
    vi.mocked(db.tokenConfig.findUnique).mockResolvedValue({
      id: 'singleton',
      enabled: false,
      chatModel: 'google/gemini-2.5-flash',
      researchModel: 'anthropic/claude-sonnet-4.6',
      reasoningModel: '',
      classificationModel: '',
      temperature: 0.4,
      maxTokensPerReply: 4096,
      monthlyBudgetUsd: 50,
      totalSpentUsd: 0,
      systemPrompt: '',
      updatedAt: new Date(),
    } as any);

    // Act
    const response = await POST(createRequest({ query: 'test' }));
    const data = await response.json();

    // Assert
    expect(response.status).toBe(503);
    expect(data.error).toContain('disabled');
  });

  it('should return 429 when budget exceeded', async () => {
    // Arrange
    vi.mocked(db.tokenConfig.findUnique).mockResolvedValue({
      id: 'singleton',
      enabled: true,
      chatModel: 'google/gemini-2.5-flash',
      researchModel: 'anthropic/claude-sonnet-4.6',
      reasoningModel: '',
      classificationModel: '',
      temperature: 0.4,
      maxTokensPerReply: 4096,
      monthlyBudgetUsd: 50,
      totalSpentUsd: 50,
      systemPrompt: '',
      updatedAt: new Date(),
    } as any);

    // Act
    const response = await POST(createRequest({ query: 'test' }));
    const data = await response.json();

    // Assert
    expect(response.status).toBe(429);
    expect(data.error).toContain('budget');
  });

  it('should return cached result on cache hit', async () => {
    // Arrange — findFirst returns a cached research message
    vi.mocked(db.tokenMessage.findFirst).mockResolvedValue({
      id: 'msg_cached',
      conversationId: 'conv_old',
      role: 'assistant',
      content: 'Cached research content',
      model: 'anthropic/claude-sonnet-4.6',
      inputTokens: 200,
      outputTokens: 400,
      costUsd: 0.003,
      latencyMs: 2000,
      messageType: 'research',
      createdAt: new Date(),
      conversation: {
        messages: [{ content: 'best model for 8gb vram' }],
      },
    } as any);

    // Act
    const response = await POST(createRequest({ query: 'best model for 8GB VRAM' }));
    const data = await response.json();

    // Assert
    expect(response.status).toBe(200);
    expect(data.cached).toBe(true);
    expect(data.result.content).toBe('Cached research content');
    expect(data.result.costUsd).toBe(0); // cached results have 0 cost
    expect(data.conversationId).toBe('conv_old');
    expect(runResearch).not.toHaveBeenCalled();
  });

  it('should create conversation when none provided', async () => {
    // Act
    await POST(createRequest({ query: 'best model for coding' }));

    // Assert
    expect(db.tokenConversation.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          userId: 'user_1',
          title: expect.stringContaining('Research:'),
        }),
      }),
    );
  });

  it('should save user query and research result to DB', async () => {
    // Act
    await POST(createRequest({ query: 'best 8GB model' }));

    // Assert — user message saved
    expect(db.tokenMessage.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          conversationId: 'conv_research',
          role: 'user',
          content: 'best 8GB model',
        }),
      }),
    );

    // Assert — assistant research message saved
    expect(db.tokenMessage.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          conversationId: 'conv_research',
          role: 'assistant',
          content: expect.stringContaining('Qwen3:8b'),
          model: 'anthropic/claude-sonnet-4.6',
          messageType: 'research',
          inputTokens: 200,
          outputTokens: 500,
          costUsd: 0.005,
        }),
      }),
    );

    // Assert — spending updated
    expect(db.tokenConfig.update).toHaveBeenCalledWith({
      where: { id: 'singleton' },
      data: { totalSpentUsd: { increment: 0.005 } },
    });
  });

  it('should create config if it does not exist', async () => {
    // Arrange
    vi.mocked(db.tokenConfig.findUnique).mockResolvedValue(null);
    vi.mocked(db.tokenConfig.create).mockResolvedValue({
      id: 'singleton',
      enabled: true,
      chatModel: 'google/gemini-2.5-flash',
      researchModel: 'anthropic/claude-sonnet-4.6',
      reasoningModel: '',
      classificationModel: '',
      temperature: 0.4,
      maxTokensPerReply: 4096,
      monthlyBudgetUsd: 50,
      totalSpentUsd: 0,
      systemPrompt: '',
      updatedAt: new Date(),
    } as any);

    // Act
    const response = await POST(createRequest({ query: 'test' }));

    // Assert
    expect(response.status).toBe(200);
    expect(db.tokenConfig.create).toHaveBeenCalledWith({
      data: { id: 'singleton' },
    });
  });

  it('should return 500 when research throws', async () => {
    // Arrange
    vi.mocked(runResearch).mockRejectedValue(new Error('OpenRouter API error'));

    // Act
    const response = await POST(createRequest({ query: 'test' }));
    const data = await response.json();

    // Assert
    expect(response.status).toBe(500);
    expect(data.error).toContain('Research failed');
  });
});
