import { describe, it, expect, vi, beforeEach } from 'vitest';
import { POST } from '@/app/api/token/chat/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: { findFirst: vi.fn(), create: vi.fn(), count: vi.fn() },
    tokenMessage: { create: vi.fn() },
    user: { findUnique: vi.fn() },
    device: { findMany: vi.fn() },
  },
}));

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

// Mock OpenRouter
vi.mock('@/lib/token/openrouter', () => ({
  callOpenRouter: vi.fn(),
  callOpenRouterStream: vi.fn(),
}));

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

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

import { auth } from '@/lib/auth';
import { callOpenRouter } from '@/lib/token/openrouter';

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

describe('POST /api/token/chat', () => {
  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',
      temperature: 0.4,
      maxTokensPerReply: 4096,
      monthlyBudgetUsd: 50,
      totalSpentUsd: 0,
      systemPrompt: '',
      researchModel: '',
      reasoningModel: '',
      classificationModel: '',
      updatedAt: new Date(),
    } as any);

    // Default: user exists on Pro plan (Token is a paid feature)
    vi.mocked(db.user.findUnique).mockResolvedValue({
      name: 'Test User',
      plan: 'pro',
    } as any);

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

    // Default: conversation count under limit
    vi.mocked(db.tokenConversation.count).mockResolvedValue(0);

    // Default: new conversation creation
    vi.mocked(db.tokenConversation.create).mockResolvedValue({
      id: 'conv_new',
      userId: 'user_1',
      title: 'Test message',
      messages: [],
      createdAt: new Date(),
      updatedAt: new Date(),
    } 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: OpenRouter response
    vi.mocked(callOpenRouter).mockResolvedValue({
      content: 'Hello from Token',
      model: 'google/gemini-2.5-flash',
      inputTokens: 100,
      outputTokens: 50,
      costUsd: 0.001,
      latencyMs: 2000,
    });
  });

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

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

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

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

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

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

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

  it('should return 400 if message is whitespace-only', async () => {
    // Act
    const response = await POST(createRequest({ message: '   ' }));
    const data = await response.json();

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

  it('should return 400 if message exceeds 10,000 characters', async () => {
    // Arrange
    const longMessage = 'a'.repeat(10_001);

    // Act
    const response = await POST(createRequest({ message: longMessage }));
    const data = await response.json();

    // Assert
    expect(response.status).toBe(400);
    expect(data.error).toContain('10,000');
  });

  it('should return 403 if user is on free plan', async () => {
    // Arrange: free-plan user
    vi.mocked(db.user.findUnique).mockResolvedValue({
      name: 'Free User',
      plan: 'free',
    } as any);

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

    // Assert
    expect(response.status).toBe(403);
    expect(data.error).toContain('Pro plan');
  });

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

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

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

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

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

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

  it('should return 429 if conversation limit reached (100)', async () => {
    // Arrange
    vi.mocked(db.tokenConversation.count).mockResolvedValue(100);

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

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

  it('should return 404 if conversation not found', async () => {
    // Arrange
    vi.mocked(db.tokenConversation.findFirst).mockResolvedValue(null);

    // Act
    const response = await POST(createRequest({ message: 'hello', conversationId: 'nonexistent' }));
    const data = await response.json();

    // Assert
    expect(response.status).toBe(404);
    expect(data.error).toContain('Conversation not found');
  });

  it('should successfully chat: creates conversation, saves messages, calls OpenRouter, returns correct shape', async () => {
    // Act
    const response = await POST(createRequest({ message: 'What models should I run?' }));
    const data = await response.json();

    // Assert
    expect(response.status).toBe(200);
    expect(data.reply).toBe('Hello from Token');
    expect(data.conversationId).toBe('conv_new');
    expect(data.model).toBe('google/gemini-2.5-flash');
    expect(data.inputTokens).toBe(100);
    expect(data.outputTokens).toBe(50);
    expect(data.costUsd).toBe(0.001);
    expect(data.latencyMs).toBe(2000);

    // Verify conversation was created
    expect(db.tokenConversation.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          userId: 'user_1',
        }),
      }),
    );

    // Verify user message was saved
    expect(db.tokenMessage.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          conversationId: 'conv_new',
          role: 'user',
          content: 'What models should I run?',
        }),
      }),
    );

    // Verify assistant message was saved
    expect(db.tokenMessage.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          conversationId: 'conv_new',
          role: 'assistant',
          content: 'Hello from Token',
          model: 'google/gemini-2.5-flash',
          inputTokens: 100,
          outputTokens: 50,
          costUsd: 0.001,
          latencyMs: 2000,
        }),
      }),
    );

    // Verify spending was updated
    expect(db.tokenConfig.update).toHaveBeenCalledWith({
      where: { id: 'singleton' },
      data: { totalSpentUsd: { increment: 0.001 } },
    });

    // Verify OpenRouter was called
    expect(callOpenRouter).toHaveBeenCalledWith(
      expect.objectContaining({
        model: 'google/gemini-2.5-flash',
        temperature: 0.4,
        maxTokens: 4096,
      }),
    );
  });

  it('should use existing conversation when conversationId is provided', async () => {
    // Arrange
    const existingConversation = {
      id: 'conv_existing',
      userId: 'user_1',
      title: 'Previous chat',
      messages: [
        { role: 'user', content: 'First message', createdAt: new Date() },
        { role: 'assistant', content: 'First reply', createdAt: new Date() },
      ],
      createdAt: new Date(),
      updatedAt: new Date(),
    };
    vi.mocked(db.tokenConversation.findFirst).mockResolvedValue(existingConversation as any);

    // Act
    const response = await POST(
      createRequest({ message: 'Follow-up question', conversationId: 'conv_existing' }),
    );
    const data = await response.json();

    // Assert
    expect(response.status).toBe(200);
    expect(data.conversationId).toBe('conv_existing');

    // Verify no new conversation was created
    expect(db.tokenConversation.create).not.toHaveBeenCalled();

    // Verify messages include history
    const callArgs = vi.mocked(callOpenRouter).mock.calls[0][0];
    // system + 2 history messages + new user message
    expect(callArgs.messages.length).toBe(4);
    expect(callArgs.messages[0].role).toBe('system');
    expect(callArgs.messages[1].content).toBe('First message');
    expect(callArgs.messages[2].content).toBe('First reply');
    expect(callArgs.messages[3].content).toBe('Follow-up question');
  });

  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',
      temperature: 0.4,
      maxTokensPerReply: 4096,
      monthlyBudgetUsd: 50,
      totalSpentUsd: 0,
      systemPrompt: '',
      researchModel: '',
      reasoningModel: '',
      classificationModel: '',
      updatedAt: new Date(),
    } as any);

    // Act
    const response = await POST(createRequest({ message: 'hello' }));

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

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

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

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

  it('should trim whitespace from message', async () => {
    // Act
    await POST(createRequest({ message: '  hello world  ' }));

    // Assert
    expect(db.tokenMessage.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          role: 'user',
          content: 'hello world',
        }),
      }),
    );
  });

  it('should generate title from first message (truncated at 80 chars)', async () => {
    // Arrange
    const longMessage = 'a'.repeat(100);

    // Act
    await POST(createRequest({ message: longMessage }));

    // Assert
    expect(db.tokenConversation.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          title: 'a'.repeat(80) + '...',
        }),
      }),
    );
  });

  it('should not add ellipsis to title for short messages', async () => {
    // Act
    await POST(createRequest({ message: 'Short message' }));

    // Assert
    expect(db.tokenConversation.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          title: 'Short message',
        }),
      }),
    );
  });
});
