import { describe, it, expect, beforeEach, vi } from 'vitest';
import { GET, POST } from '@/app/api/costs/route';
import { DELETE } from '@/app/api/costs/[id]/route';
import { NextRequest } from 'next/server';
import { db } from '@/lib/db';

// Mock the database
vi.mock('@/lib/db', () => ({
  db: {
    costEntry: {
      findMany: vi.fn(),
      findUnique: vi.fn(),
      create: vi.fn(),
      delete: vi.fn(),
    },
  },
}));

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

import { auth } from '@/lib/auth';

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

  describe('GET /api/costs', () => {
    it('should reject unauthenticated requests', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue(null as any);

      const request = new NextRequest('http://localhost:3000/api/costs', {
        method: 'GET',
      });

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

      // Assert
      expect(response.status).toBe(401);
      expect(data.error).toBe('Unauthorized');
      expect(db.costEntry.findMany).not.toHaveBeenCalled();
    });

    it('should return all cost entries with totals for authenticated user', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      const mockEntries = [
        {
          id: '1',
          userId: 'user-1',
          provider: 'openai',
          amount: 100.50,
          period: '2026-01',
          category: 'api',
          note: null,
          createdAt: new Date(),
        },
        {
          id: '2',
          userId: 'user-1',
          provider: 'anthropic',
          amount: 50.25,
          period: '2026-01',
          category: 'subscription',
          note: 'Monthly plan',
          createdAt: new Date(),
        },
        {
          id: '3',
          userId: 'user-1',
          provider: 'openai',
          amount: 75.00,
          period: '2026-02',
          category: 'api',
          note: null,
          createdAt: new Date(),
        },
      ];

      vi.mocked(db.costEntry.findMany).mockResolvedValue(mockEntries as any);

      const request = new NextRequest('http://localhost:3000/api/costs', {
        method: 'GET',
      });

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

      // Assert
      expect(response.status).toBe(200);
      expect(data.entries).toHaveLength(3);
      expect(data.totals).toEqual({
        byProvider: {
          openai: 175.50,
          anthropic: 50.25,
        },
        byMonth: {
          '2026-01': 150.75,
          '2026-02': 75.00,
        },
        total: 225.75,
      });
      expect(db.costEntry.findMany).toHaveBeenCalledWith({
        where: { userId: 'user-1' },
        orderBy: [{ period: 'desc' }, { createdAt: 'desc' }],
      });
    });

    it('should filter by period when period param is provided', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      vi.mocked(db.costEntry.findMany).mockResolvedValue([]);

      const request = new NextRequest('http://localhost:3000/api/costs?period=2026-01', {
        method: 'GET',
      });

      // Act
      const response = await GET(request);

      // Assert
      expect(response.status).toBe(200);
      expect(db.costEntry.findMany).toHaveBeenCalledWith({
        where: { userId: 'user-1', period: '2026-01' },
        orderBy: [{ period: 'desc' }, { createdAt: 'desc' }],
      });
    });

    it('should filter by provider when provider param is provided', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      vi.mocked(db.costEntry.findMany).mockResolvedValue([]);

      const request = new NextRequest('http://localhost:3000/api/costs?provider=openai', {
        method: 'GET',
      });

      // Act
      const response = await GET(request);

      // Assert
      expect(response.status).toBe(200);
      expect(db.costEntry.findMany).toHaveBeenCalledWith({
        where: { userId: 'user-1', provider: 'openai' },
        orderBy: [{ period: 'desc' }, { createdAt: 'desc' }],
      });
    });

    it('should filter by both period and provider when both params are provided', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      vi.mocked(db.costEntry.findMany).mockResolvedValue([]);

      const request = new NextRequest('http://localhost:3000/api/costs?period=2026-01&provider=anthropic', {
        method: 'GET',
      });

      // Act
      const response = await GET(request);

      // Assert
      expect(response.status).toBe(200);
      expect(db.costEntry.findMany).toHaveBeenCalledWith({
        where: { userId: 'user-1', period: '2026-01', provider: 'anthropic' },
        orderBy: [{ period: 'desc' }, { createdAt: 'desc' }],
      });
    });

    it('should handle database errors gracefully', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      vi.mocked(db.costEntry.findMany).mockRejectedValue(new Error('Database error'));

      const request = new NextRequest('http://localhost:3000/api/costs', {
        method: 'GET',
      });

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

      // Assert
      expect(response.status).toBe(500);
      expect(data.error).toBe('Failed to fetch cost entries');
    });
  });

  describe('POST /api/costs', () => {
    it('should reject unauthenticated requests', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue(null as any);

      const request = new NextRequest('http://localhost:3000/api/costs', {
        method: 'POST',
        body: JSON.stringify({
          provider: 'openai',
          amount: 100,
          period: '2026-01',
        }),
      });

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

      // Assert
      expect(response.status).toBe(401);
      expect(data.error).toBe('Unauthorized');
      expect(db.costEntry.create).not.toHaveBeenCalled();
    });

    it('should create cost entry with valid data', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      const mockEntry = {
        id: '1',
        userId: 'user-1',
        provider: 'openai',
        amount: 100.50,
        period: '2026-01',
        category: 'api',
        note: 'Test note',
        createdAt: new Date(),
      };

      vi.mocked(db.costEntry.create).mockResolvedValue(mockEntry as any);

      const request = new NextRequest('http://localhost:3000/api/costs', {
        method: 'POST',
        body: JSON.stringify({
          provider: 'openai',
          amount: 100.50,
          period: '2026-01',
          category: 'api',
          note: 'Test note',
        }),
      });

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

      // Assert
      expect(response.status).toBe(201);
      expect(data.entry).toMatchObject({
        id: '1',
        userId: 'user-1',
        provider: 'openai',
        amount: 100.50,
        period: '2026-01',
        category: 'api',
        note: 'Test note',
      });
      expect(db.costEntry.create).toHaveBeenCalledWith({
        data: {
          userId: 'user-1',
          provider: 'openai',
          amount: 100.50,
          period: '2026-01',
          category: 'api',
          note: 'Test note',
        },
      });
    });

    it('should use default category "api" when not provided', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      const mockEntry = {
        id: '1',
        userId: 'user-1',
        provider: 'anthropic',
        amount: 50,
        period: '2026-02',
        category: 'api',
        note: null,
        createdAt: new Date(),
      };

      vi.mocked(db.costEntry.create).mockResolvedValue(mockEntry as any);

      const request = new NextRequest('http://localhost:3000/api/costs', {
        method: 'POST',
        body: JSON.stringify({
          provider: 'anthropic',
          amount: 50,
          period: '2026-02',
        }),
      });

      // Act
      const response = await POST(request);

      // Assert
      expect(response.status).toBe(201);
      expect(db.costEntry.create).toHaveBeenCalledWith({
        data: {
          userId: 'user-1',
          provider: 'anthropic',
          amount: 50,
          period: '2026-02',
          category: 'api',
          note: null,
        },
      });
    });

    it('should reject request with missing provider', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      const request = new NextRequest('http://localhost:3000/api/costs', {
        method: 'POST',
        body: JSON.stringify({
          amount: 100,
          period: '2026-01',
        }),
      });

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

      // Assert
      expect(response.status).toBe(400);
      expect(data.error).toContain('required');
      expect(db.costEntry.create).not.toHaveBeenCalled();
    });

    it('should reject request with missing amount', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      const request = new NextRequest('http://localhost:3000/api/costs', {
        method: 'POST',
        body: JSON.stringify({
          provider: 'openai',
          period: '2026-01',
        }),
      });

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

      // Assert
      expect(response.status).toBe(400);
      expect(data.error).toContain('required');
      expect(db.costEntry.create).not.toHaveBeenCalled();
    });

    it('should reject request with missing period', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      const request = new NextRequest('http://localhost:3000/api/costs', {
        method: 'POST',
        body: JSON.stringify({
          provider: 'openai',
          amount: 100,
        }),
      });

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

      // Assert
      expect(response.status).toBe(400);
      expect(data.error).toContain('required');
      expect(db.costEntry.create).not.toHaveBeenCalled();
    });

    it('should reject request with invalid provider', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      const request = new NextRequest('http://localhost:3000/api/costs', {
        method: 'POST',
        body: JSON.stringify({
          provider: 'invalid-provider',
          amount: 100,
          period: '2026-01',
        }),
      });

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

      // Assert
      expect(response.status).toBe(400);
      expect(data.error).toContain('Invalid provider');
      expect(db.costEntry.create).not.toHaveBeenCalled();
    });

    it('should reject request with negative amount', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      const request = new NextRequest('http://localhost:3000/api/costs', {
        method: 'POST',
        body: JSON.stringify({
          provider: 'openai',
          amount: -50,
          period: '2026-01',
        }),
      });

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

      // Assert
      expect(response.status).toBe(400);
      expect(data.error).toContain('non-negative');
      expect(db.costEntry.create).not.toHaveBeenCalled();
    });

    it('should reject request with non-numeric amount', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      const request = new NextRequest('http://localhost:3000/api/costs', {
        method: 'POST',
        body: JSON.stringify({
          provider: 'openai',
          amount: 'not-a-number',
          period: '2026-01',
        }),
      });

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

      // Assert
      expect(response.status).toBe(400);
      expect(data.error).toContain('non-negative number');
      expect(db.costEntry.create).not.toHaveBeenCalled();
    });

    it('should reject request with invalid period format', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      const request = new NextRequest('http://localhost:3000/api/costs', {
        method: 'POST',
        body: JSON.stringify({
          provider: 'openai',
          amount: 100,
          period: '2026/01', // Invalid format
        }),
      });

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

      // Assert
      expect(response.status).toBe(400);
      expect(data.error).toContain('YYYY-MM format');
      expect(db.costEntry.create).not.toHaveBeenCalled();
    });

    it('should reject request with invalid category', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      const request = new NextRequest('http://localhost:3000/api/costs', {
        method: 'POST',
        body: JSON.stringify({
          provider: 'openai',
          amount: 100,
          period: '2026-01',
          category: 'invalid-category',
        }),
      });

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

      // Assert
      expect(response.status).toBe(400);
      expect(data.error).toContain('Invalid category');
      expect(db.costEntry.create).not.toHaveBeenCalled();
    });

    it('should handle database errors gracefully', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      vi.mocked(db.costEntry.create).mockRejectedValue(new Error('Database error'));

      const request = new NextRequest('http://localhost:3000/api/costs', {
        method: 'POST',
        body: JSON.stringify({
          provider: 'openai',
          amount: 100,
          period: '2026-01',
        }),
      });

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

      // Assert
      expect(response.status).toBe(500);
      expect(data.error).toBe('Failed to create cost entry');
    });
  });

  describe('DELETE /api/costs/[id]', () => {
    it('should reject unauthenticated requests', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue(null as any);

      const request = new NextRequest('http://localhost:3000/api/costs/1', {
        method: 'DELETE',
      });
      const params = Promise.resolve({ id: '1' });

      // Act
      const response = await DELETE(request, { params });
      const data = await response.json();

      // Assert
      expect(response.status).toBe(401);
      expect(data.error).toBe('Unauthorized');
      expect(db.costEntry.delete).not.toHaveBeenCalled();
    });

    it('should delete cost entry owned by authenticated user', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      const mockEntry = {
        id: '1',
        userId: 'user-1',
        provider: 'openai',
        amount: 100,
        period: '2026-01',
        category: 'api',
        note: null,
        createdAt: new Date(),
      };

      vi.mocked(db.costEntry.findUnique).mockResolvedValue(mockEntry as any);
      vi.mocked(db.costEntry.delete).mockResolvedValue(mockEntry as any);

      const request = new NextRequest('http://localhost:3000/api/costs/1', {
        method: 'DELETE',
      });
      const params = Promise.resolve({ id: '1' });

      // Act
      const response = await DELETE(request, { params });
      const data = await response.json();

      // Assert
      expect(response.status).toBe(200);
      expect(data.message).toBe('Cost entry deleted');
      expect(db.costEntry.findUnique).toHaveBeenCalledWith({ where: { id: '1' } });
      expect(db.costEntry.delete).toHaveBeenCalledWith({ where: { id: '1' } });
    });

    it('should return 404 if cost entry does not exist', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      vi.mocked(db.costEntry.findUnique).mockResolvedValue(null);

      const request = new NextRequest('http://localhost:3000/api/costs/999', {
        method: 'DELETE',
      });
      const params = Promise.resolve({ id: '999' });

      // Act
      const response = await DELETE(request, { params });
      const data = await response.json();

      // Assert
      expect(response.status).toBe(404);
      expect(data.error).toBe('Cost entry not found');
      expect(db.costEntry.delete).not.toHaveBeenCalled();
    });

    it('should return 403 if user tries to delete another user\'s cost entry', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      const mockEntry = {
        id: '1',
        userId: 'user-2', // Different user
        provider: 'openai',
        amount: 100,
        period: '2026-01',
        category: 'api',
        note: null,
        createdAt: new Date(),
      };

      vi.mocked(db.costEntry.findUnique).mockResolvedValue(mockEntry as any);

      const request = new NextRequest('http://localhost:3000/api/costs/1', {
        method: 'DELETE',
      });
      const params = Promise.resolve({ id: '1' });

      // Act
      const response = await DELETE(request, { params });
      const data = await response.json();

      // Assert
      expect(response.status).toBe(403);
      expect(data.error).toBe('You can only delete your own cost entries');
      expect(db.costEntry.delete).not.toHaveBeenCalled();
    });

    it('should handle database errors gracefully', async () => {
      // Arrange
      vi.mocked(auth).mockResolvedValue({
        user: { id: 'user-1', email: 'test@example.com' },
      } as any);

      vi.mocked(db.costEntry.findUnique).mockRejectedValue(new Error('Database error'));

      const request = new NextRequest('http://localhost:3000/api/costs/1', {
        method: 'DELETE',
      });
      const params = Promise.resolve({ id: '1' });

      // Act
      const response = await DELETE(request, { params });
      const data = await response.json();

      // Assert
      expect(response.status).toBe(500);
      expect(data.error).toBe('Failed to delete cost entry');
    });
  });
});
