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

// Mock database
vi.mock('@/lib/db', () => ({
  db: {
    tokenFleetAction: {
      findMany: vi.fn(),
      findFirst: vi.fn(),
      create: vi.fn(),
      update: vi.fn(),
    },
    tokenConversation: { findFirst: vi.fn() },
    device: { findFirst: 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 dispatchFleetAction
vi.mock('@/lib/token/agents/fleet-manager', () => ({
  dispatchFleetAction: vi.fn(),
}));

import { auth } from '@/lib/auth';
import { dispatchFleetAction } from '@/lib/token/agents/fleet-manager';

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

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

function createGetRequest(params: Record<string, string> = {}): NextRequest {
  const url = new URL('http://localhost:3000/api/token/actions');
  Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
  return new NextRequest(url);
}

describe('GET /api/token/actions', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should return actions for authenticated user', async () => {
    // Arrange
    vi.mocked(auth).mockResolvedValue({
      user: { id: 'user_1', email: 'test@test.com' },
    } as any);

    const mockActions = [
      { id: 'act_1', type: 'pull_model', status: 'pending', createdAt: new Date() },
      { id: 'act_2', type: 'remove_model', status: 'success', createdAt: new Date() },
    ];
    vi.mocked(db.tokenFleetAction.findMany).mockResolvedValue(mockActions as any);

    // Act
    const response = await GET(createGetRequest());
    const data = await response.json();

    // Assert
    expect(response.status).toBe(200);
    expect(data.actions).toHaveLength(2);
  });

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

    // Act
    const response = await GET(createGetRequest());
    const data = await response.json();

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

  it('should filter by conversationId and status', async () => {
    // Arrange
    vi.mocked(auth).mockResolvedValue({
      user: { id: 'user_1', email: 'test@test.com' },
    } as any);
    vi.mocked(db.tokenFleetAction.findMany).mockResolvedValue([]);

    // Act
    await GET(createGetRequest({ conversationId: 'conv_1', status: 'pending' }));

    // Assert
    expect(db.tokenFleetAction.findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: expect.objectContaining({
          conversationId: 'conv_1',
          status: 'pending',
          conversation: { userId: 'user_1' },
        }),
      }),
    );
  });
});

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

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

  it('should create actions from proposals', async () => {
    // Arrange
    vi.mocked(db.tokenConversation.findFirst).mockResolvedValue({
      id: 'conv_1',
      userId: 'user_1',
    } as any);

    const mockCreated = { id: 'act_1', type: 'pull_model', status: 'pending' };
    vi.mocked(db.tokenFleetAction.create).mockResolvedValue(mockCreated as any);

    const body = {
      conversationId: 'conv_1',
      actions: [
        { type: 'pull_model', deviceId: 'dev_1', payload: { model: 'qwen3:8b' } },
        { type: 'remove_model', deviceId: 'dev_1', payload: { model: 'old:7b' } },
      ],
    };

    // Act
    const response = await POST(createPostRequest(body));
    const data = await response.json();

    // Assert
    expect(response.status).toBe(201);
    expect(data.actions).toHaveLength(2);
    expect(db.tokenFleetAction.create).toHaveBeenCalledTimes(2);
  });

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

    // Act
    const response = await POST(
      createPostRequest({
        conversationId: 'conv_1',
        actions: [{ type: 'pull_model', deviceId: 'dev_1' }],
      }),
    );
    const data = await response.json();

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

  it('should return 400 without conversationId', async () => {
    // Act
    const response = await POST(
      createPostRequest({ actions: [{ type: 'pull_model', deviceId: 'dev_1' }] }),
    );
    const data = await response.json();

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

  it('should return 400 without actions array', async () => {
    // Act
    const response = await POST(createPostRequest({ conversationId: 'conv_1' }));
    const data = await response.json();

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

  it('should return 400 with empty actions array', async () => {
    // Act
    const response = await POST(
      createPostRequest({ conversationId: 'conv_1', actions: [] }),
    );
    const data = await response.json();

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

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

    // Act
    const response = await POST(
      createPostRequest({
        conversationId: 'conv_nonexistent',
        actions: [{ type: 'pull_model', deviceId: 'dev_1' }],
      }),
    );
    const data = await response.json();

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

  it('should filter out invalid action types', async () => {
    // Arrange
    vi.mocked(db.tokenConversation.findFirst).mockResolvedValue({
      id: 'conv_1',
      userId: 'user_1',
    } as any);

    const mockCreated = { id: 'act_1', type: 'pull_model', status: 'pending' };
    vi.mocked(db.tokenFleetAction.create).mockResolvedValue(mockCreated as any);

    const body = {
      conversationId: 'conv_1',
      actions: [
        { type: 'invalid_type', deviceId: 'dev_1' },
        { type: 'pull_model', deviceId: 'dev_1', payload: { model: 'qwen3:8b' } },
      ],
    };

    // Act
    const response = await POST(createPostRequest(body));
    const data = await response.json();

    // Assert
    expect(response.status).toBe(201);
    expect(db.tokenFleetAction.create).toHaveBeenCalledTimes(1);
    expect(data.actions).toHaveLength(1);
  });

  it('should limit to 5 actions', async () => {
    // Arrange
    vi.mocked(db.tokenConversation.findFirst).mockResolvedValue({
      id: 'conv_1',
      userId: 'user_1',
    } as any);

    vi.mocked(db.tokenFleetAction.create).mockResolvedValue({
      id: 'act_x',
      type: 'pull_model',
      status: 'pending',
    } as any);

    const tenActions = Array.from({ length: 10 }, (_, i) => ({
      type: 'pull_model',
      deviceId: `dev_${i}`,
      payload: { model: `model_${i}` },
    }));

    // Act
    const response = await POST(
      createPostRequest({ conversationId: 'conv_1', actions: tenActions }),
    );

    // Assert
    expect(response.status).toBe(201);
    expect(db.tokenFleetAction.create).toHaveBeenCalledTimes(5);
  });

  it('should return 400 when all actions are invalid (no deviceId)', async () => {
    // Arrange
    vi.mocked(db.tokenConversation.findFirst).mockResolvedValue({
      id: 'conv_1',
      userId: 'user_1',
    } as any);

    const body = {
      conversationId: 'conv_1',
      actions: [
        { type: 'pull_model' }, // missing deviceId
        { type: 'invalid_type', deviceId: 'dev_1' }, // invalid type
      ],
    };

    // Act
    const response = await POST(createPostRequest(body));
    const data = await response.json();

    // Assert
    expect(response.status).toBe(400);
    expect(data.error).toContain('No valid actions');
  });
});

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

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

  it('should approve and dispatch action', async () => {
    // Arrange
    const mockAction = {
      id: 'act_1',
      conversationId: 'conv_1',
      deviceId: 'dev_1',
      type: 'pull_model',
      payload: '{"model":"qwen3:8b"}',
      status: 'pending',
    };
    vi.mocked(db.tokenFleetAction.findFirst).mockResolvedValue(mockAction as any);

    const mockDevice = {
      id: 'dev_1',
      userId: 'user_1',
      status: 'online',
      tunnelUrl: 'http://desktop.local:11434',
    };
    vi.mocked(db.device.findFirst).mockResolvedValue(mockDevice as any);

    vi.mocked(dispatchFleetAction).mockResolvedValue({
      success: true,
      result: 'Model pulled successfully',
    });

    const updatedAction = { ...mockAction, status: 'success' };
    vi.mocked(db.tokenFleetAction.update).mockResolvedValue(updatedAction as any);

    // Act
    const response = await PATCH(
      createPatchRequest({ actionId: 'act_1', status: 'approved' }),
    );
    await response.json();

    // Assert
    expect(response.status).toBe(200);
    expect(dispatchFleetAction).toHaveBeenCalledWith(
      'http://desktop.local:11434',
      'pull_model',
      { model: 'qwen3:8b' },
    );
    // First update: in_progress, final update: success
    expect(db.tokenFleetAction.update).toHaveBeenCalledTimes(2);
  });

  it('should reject pending action', async () => {
    // Arrange
    const mockAction = {
      id: 'act_1',
      type: 'pull_model',
      status: 'pending',
    };
    vi.mocked(db.tokenFleetAction.findFirst).mockResolvedValue(mockAction as any);

    const rejectedAction = { ...mockAction, status: 'rejected' };
    vi.mocked(db.tokenFleetAction.update).mockResolvedValue(rejectedAction as any);

    // Act
    const response = await PATCH(
      createPatchRequest({ actionId: 'act_1', status: 'rejected' }),
    );
    await response.json();

    // Assert
    expect(response.status).toBe(200);
    expect(db.tokenFleetAction.update).toHaveBeenCalledWith({
      where: { id: 'act_1' },
      data: { status: 'rejected' },
    });
    expect(dispatchFleetAction).not.toHaveBeenCalled();
  });

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

    // Act
    const response = await PATCH(
      createPatchRequest({ actionId: 'act_1', status: 'approved' }),
    );
    const data = await response.json();

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

  it('should return 400 without actionId', async () => {
    // Act
    const response = await PATCH(createPatchRequest({ status: 'approved' }));
    const data = await response.json();

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

  it('should return 400 without status', async () => {
    // Act
    const response = await PATCH(createPatchRequest({ actionId: 'act_1' }));
    const data = await response.json();

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

  it('should return 400 for invalid status value', async () => {
    // Act
    const response = await PATCH(
      createPatchRequest({ actionId: 'act_1', status: 'invalid_status' }),
    );
    const data = await response.json();

    // Assert
    expect(response.status).toBe(400);
    expect(data.error).toContain("'approved' or 'rejected'");
  });

  it('should return 404 for non-pending action', async () => {
    // Arrange — findFirst returns null because action is not pending
    vi.mocked(db.tokenFleetAction.findFirst).mockResolvedValue(null);

    // Act
    const response = await PATCH(
      createPatchRequest({ actionId: 'act_nonexistent', status: 'approved' }),
    );
    const data = await response.json();

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

  it('should handle device offline', async () => {
    // Arrange
    const mockAction = {
      id: 'act_1',
      deviceId: 'dev_1',
      type: 'pull_model',
      payload: '{"model":"qwen3:8b"}',
      status: 'pending',
    };
    vi.mocked(db.tokenFleetAction.findFirst).mockResolvedValue(mockAction as any);

    const offlineDevice = {
      id: 'dev_1',
      userId: 'user_1',
      status: 'offline',
      tunnelUrl: 'http://desktop.local:11434',
    };
    vi.mocked(db.device.findFirst).mockResolvedValue(offlineDevice as any);
    vi.mocked(db.tokenFleetAction.update).mockResolvedValue({} as any);

    // Act
    const response = await PATCH(
      createPatchRequest({ actionId: 'act_1', status: 'approved' }),
    );
    const data = await response.json();

    // Assert
    expect(response.status).toBe(409);
    expect(data.error).toContain('offline');
    // Should update action status to failed
    expect(db.tokenFleetAction.update).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: 'act_1' },
        data: expect.objectContaining({ status: 'failed' }),
      }),
    );
  });

  it('should handle device not found', async () => {
    // Arrange
    const mockAction = {
      id: 'act_1',
      deviceId: 'dev_missing',
      type: 'pull_model',
      payload: '{"model":"qwen3:8b"}',
      status: 'pending',
    };
    vi.mocked(db.tokenFleetAction.findFirst).mockResolvedValue(mockAction as any);
    vi.mocked(db.device.findFirst).mockResolvedValue(null);
    vi.mocked(db.tokenFleetAction.update).mockResolvedValue({} as any);

    // Act
    const response = await PATCH(
      createPatchRequest({ actionId: 'act_1', status: 'approved' }),
    );
    const data = await response.json();

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

  it('should handle dispatch failure', async () => {
    // Arrange
    const mockAction = {
      id: 'act_1',
      deviceId: 'dev_1',
      type: 'pull_model',
      payload: '{"model":"qwen3:8b"}',
      status: 'pending',
    };
    vi.mocked(db.tokenFleetAction.findFirst).mockResolvedValue(mockAction as any);

    const mockDevice = {
      id: 'dev_1',
      userId: 'user_1',
      status: 'online',
      tunnelUrl: 'http://desktop.local:11434',
    };
    vi.mocked(db.device.findFirst).mockResolvedValue(mockDevice as any);

    vi.mocked(dispatchFleetAction).mockResolvedValue({
      success: false,
      result: 'Connection refused',
    });

    const failedAction = { ...mockAction, status: 'failed' };
    vi.mocked(db.tokenFleetAction.update).mockResolvedValue(failedAction as any);

    // Act
    const response = await PATCH(
      createPatchRequest({ actionId: 'act_1', status: 'approved' }),
    );
    await response.json();

    // Assert
    expect(response.status).toBe(200);
    // Final update should be 'failed'
    expect(db.tokenFleetAction.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          status: 'failed',
          result: JSON.stringify({ success: false, result: 'Connection refused' }),
        }),
      }),
    );
  });
});
