import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { planDeployment } from '@/lib/token/agents/deployment-planner';
import type { FleetDevice } from '@/lib/token/agents/fleet-manager';

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

describe('planDeployment', () => {
  const originalEnv = process.env.OPENROUTER_API_KEY;

  beforeEach(() => {
    vi.clearAllMocks();
    process.env.OPENROUTER_API_KEY = 'test-openrouter-key';
  });

  afterEach(() => {
    vi.restoreAllMocks();
    if (originalEnv !== undefined) {
      process.env.OPENROUTER_API_KEY = originalEnv;
    } else {
      delete process.env.OPENROUTER_API_KEY;
    }
  });

  const testDevices: FleetDevice[] = [
    {
      id: 'dev_1',
      name: 'Desktop',
      status: 'online',
      models: ['qwen3:8b'],
      hardware: 'RTX 4080 16GB',
      tunnelUrl: 'http://desktop.local:11434',
    },
    {
      id: 'dev_2',
      name: 'Server',
      status: 'online',
      models: ['llama3.3:70b'],
      hardware: 'RTX A6000 48GB',
      tunnelUrl: 'http://server.local:11434',
    },
  ];

  function mockFetchResponse(
    content: string,
    options: { ok?: boolean; status?: number; costHeader?: string } = {},
  ) {
    const { ok = true, status = 200, costHeader = '0.01' } = options;
    const mockResponse = {
      ok,
      status,
      json: async () => ({
        choices: [{ message: { content } }],
        usage: { prompt_tokens: 400, completion_tokens: 500 },
        model: 'deepseek/deepseek-r1',
      }),
      text: async () => content,
      headers: new Headers({
        ...(costHeader ? { 'x-openrouter-cost': costHeader } : {}),
      }),
    };
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse));
  }

  it('should generate structured deployment plan', async () => {
    // Arrange
    const llmResponse = JSON.stringify({
      summary: 'Optimized deployment for 2 devices.',
      plan: [
        {
          deviceId: 'dev_1',
          deviceName: 'Desktop',
          currentModels: ['qwen3:8b'],
          proposedModels: ['qwen3:8b', 'qwen2.5-coder:7b'],
          toInstall: ['qwen2.5-coder:7b'],
          toRemove: [],
          vramUsed: '10.2 GB',
          vramTotal: '16 GB',
          rationale: 'Adding coding model to Desktop',
        },
        {
          deviceId: 'dev_2',
          deviceName: 'Server',
          currentModels: ['llama3.3:70b'],
          proposedModels: ['llama3.3:70b', 'deepseek-r1:14b'],
          toInstall: ['deepseek-r1:14b'],
          toRemove: [],
          vramUsed: '49 GB',
          vramTotal: '48 GB',
          rationale: 'Adding reasoning model',
        },
      ],
      capabilityCoverage: { general: true, coding: true, reasoning: true, vision: false },
      estimatedActions: 2,
    });
    mockFetchResponse(llmResponse);

    // Act
    const result = await planDeployment(
      'optimize my fleet',
      'deepseek/deepseek-r1',
      testDevices,
    );

    // Assert
    expect(result.summary).toBe('Optimized deployment for 2 devices.');
    expect(result.plan).toHaveLength(2);
    expect(result.plan[0].deviceName).toBe('Desktop');
    expect(result.plan[0].toInstall).toEqual(['qwen2.5-coder:7b']);
    expect(result.capabilityCoverage.general).toBe(true);
    expect(result.capabilityCoverage.vision).toBe(false);
    expect(result.estimatedActions).toBe(2);
    expect(result.model).toBe('deepseek/deepseek-r1');
    expect(result.inputTokens).toBe(400);
    expect(result.outputTokens).toBe(500);
  });

  it('should convert plan entries to fleet action proposals', async () => {
    // Arrange
    const llmResponse = JSON.stringify({
      summary: 'Rearranging models.',
      plan: [
        {
          deviceId: 'dev_1',
          deviceName: 'Desktop',
          currentModels: ['qwen3:8b', 'old-model:7b'],
          proposedModels: ['qwen3:8b', 'new-model:7b'],
          toInstall: ['new-model:7b'],
          toRemove: ['old-model:7b'],
          vramUsed: '10 GB',
          vramTotal: '16 GB',
          rationale: 'Swapping old for new',
        },
      ],
      capabilityCoverage: {},
      estimatedActions: 2,
    });
    mockFetchResponse(llmResponse);

    // Act
    const result = await planDeployment('rearrange', 'deepseek/deepseek-r1', testDevices);

    // Assert — toInstall => pull_model, toRemove => remove_model
    expect(result.actions).toHaveLength(2);
    const pullAction = result.actions.find((a) => a.type === 'pull_model');
    const removeAction = result.actions.find((a) => a.type === 'remove_model');
    expect(pullAction).toBeDefined();
    expect(pullAction!.payload.model).toBe('new-model:7b');
    expect(pullAction!.deviceId).toBe('dev_1');
    expect(removeAction).toBeDefined();
    expect(removeAction!.payload.model).toBe('old-model:7b');
  });

  it('should build human-readable markdown with table', async () => {
    // Arrange
    const llmResponse = JSON.stringify({
      summary: 'Fleet optimized.',
      plan: [
        {
          deviceId: 'dev_1',
          deviceName: 'Desktop',
          currentModels: ['qwen3:8b'],
          proposedModels: ['qwen3:8b', 'qwen2.5-coder:7b'],
          toInstall: ['qwen2.5-coder:7b'],
          toRemove: [],
          vramUsed: '10 GB',
          vramTotal: '16 GB',
          rationale: 'Adding coding capability',
        },
      ],
      capabilityCoverage: { general: true, coding: true },
      estimatedActions: 1,
    });
    mockFetchResponse(llmResponse);

    // Act
    const result = await planDeployment(
      'optimize fleet',
      'deepseek/deepseek-r1',
      testDevices,
    );

    // Assert — content should have markdown table
    expect(result.content).toContain('## Deployment Plan');
    expect(result.content).toContain('| Device |');
    expect(result.content).toContain('Desktop');
    expect(result.content).toContain('Install `qwen2.5-coder:7b`');
    expect(result.content).toContain('Coverage');
    expect(result.content).toContain('1 actions');
  });

  it('should handle empty plan gracefully', async () => {
    // Arrange
    const llmResponse = JSON.stringify({
      summary: 'Fleet is already optimized.',
      plan: [],
      capabilityCoverage: {},
      estimatedActions: 0,
    });
    mockFetchResponse(llmResponse);

    // Act
    const result = await planDeployment(
      'optimize fleet',
      'deepseek/deepseek-r1',
      testDevices,
    );

    // Assert
    expect(result.plan).toHaveLength(0);
    expect(result.actions).toHaveLength(0);
    expect(result.summary).toBe('Fleet is already optimized.');
    // No table in content when plan is empty
    expect(result.content).not.toContain('| Device |');
  });

  it('should throw on missing API key', async () => {
    // Arrange
    delete process.env.OPENROUTER_API_KEY;

    // Act & Assert
    await expect(
      planDeployment('optimize', 'deepseek/deepseek-r1', testDevices),
    ).rejects.toThrow('OPENROUTER_API_KEY not configured');
  });

  it('should throw on API error', async () => {
    // Arrange
    mockFetchResponse('Server error', { ok: false, status: 503 });

    // Act & Assert
    await expect(
      planDeployment('optimize', 'deepseek/deepseek-r1', testDevices),
    ).rejects.toThrow('Deployment planner API error (503)');
  });

  it('should handle malformed JSON with invalid braces', async () => {
    // Arrange — response has braces but invalid JSON, triggering catch block
    mockFetchResponse('Here is my plan {invalid json: not valid!!!}');

    // Act
    const result = await planDeployment(
      'optimize fleet',
      'deepseek/deepseek-r1',
      testDevices,
    );

    // Assert — catch sets summary = rawContent, plan is empty
    expect(result.summary).toContain('Here is my plan');
    expect(result.plan).toHaveLength(0);
    expect(result.actions).toHaveLength(0);
  });

  it('should return empty summary when no JSON found in response', async () => {
    // Arrange — no braces at all, jsonMatch is null
    mockFetchResponse('Plain text analysis with no JSON structure.');

    // Act
    const result = await planDeployment(
      'optimize fleet',
      'deepseek/deepseek-r1',
      testDevices,
    );

    // Assert — no match, summary stays at initial ""
    expect(result.summary).toBe('');
    expect(result.plan).toHaveLength(0);
    expect(result.actions).toHaveLength(0);
  });

  it('should extract cost from headers', async () => {
    // Arrange
    const llmResponse = JSON.stringify({
      summary: 'Plan ready.',
      plan: [],
      capabilityCoverage: {},
      estimatedActions: 0,
    });
    mockFetchResponse(llmResponse, { costHeader: '0.025' });

    // Act
    const result = await planDeployment(
      'optimize fleet',
      'deepseek/deepseek-r1',
      testDevices,
    );

    // Assert
    expect(result.costUsd).toBe(0.025);
  });
});
