import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
import {
  fetchPrice,
  fetchPrices,
  fetchAppraisal,
  appraiseItems,
  isPositiveNumber,
  isNonEmptyString,
  markets,
  AppraisalDesignation,
  AppraisalPricing,
  AppraisalPricingVariant,
  clearCache,
} from './janice.ts';

// Mock fetch globally
const originalFetch = global.fetch;
let mockFetch: any;

beforeEach(() => {
  clearCache();
  mockFetch = mock(() => Promise.resolve({
    ok: true,
    json: () => Promise.resolve({}),
  }));
  global.fetch = mockFetch;
});

afterEach(() => {
  global.fetch = originalFetch;
  // Clear cache between tests
  // Note: Since cache is internal to the module, we can't directly clear it
  // In a real scenario, you might want to expose a clearCache function
});

describe('Validation Functions', () => {
  describe('isPositiveNumber', () => {
    it('should return true for positive numbers', () => {
      expect(isPositiveNumber(1)).toBe(true);
      expect(isPositiveNumber(100)).toBe(true);
      expect(isPositiveNumber(0.5)).toBe(true);
    });

    it('should return false for non-positive numbers', () => {
      expect(isPositiveNumber(0)).toBe(false);
      expect(isPositiveNumber(-1)).toBe(false);
      expect(isPositiveNumber(-0.5)).toBe(false);
    });

    it('should return false for non-numbers', () => {
      expect(isPositiveNumber('1')).toBe(false);
      expect(isPositiveNumber(null)).toBe(false);
      expect(isPositiveNumber(undefined)).toBe(false);
      expect(isPositiveNumber(NaN)).toBe(false);
      expect(isPositiveNumber(Infinity)).toBe(false);
    });
  });

  describe('isNonEmptyString', () => {
    it('should return true for non-empty strings', () => {
      expect(isNonEmptyString('test')).toBe(true);
      expect(isNonEmptyString(' a ')).toBe(true);
    });

    it('should return false for empty strings', () => {
      expect(isNonEmptyString('')).toBe(false);
      expect(isNonEmptyString('   ')).toBe(false);
    });

    it('should return false for non-strings', () => {
      expect(isNonEmptyString(123)).toBe(false);
      expect(isNonEmptyString(null)).toBe(false);
      expect(isNonEmptyString(undefined)).toBe(false);
    });
  });
});

describe('Constants', () => {
  it('should have correct markets array', () => {
    expect(markets).toHaveLength(8);
    expect(markets[0]).toEqual({ id: 2, name: 'Jita 4-4' });
    expect(markets).toContainEqual({ id: 117, name: 'Dodixie' });
  });

  it('should have correct enum values', () => {
    expect(AppraisalDesignation.Appraisal).toBe(AppraisalDesignation.Appraisal);
    expect(AppraisalPricing.Buy).toBe(AppraisalPricing.Buy);
    expect(AppraisalPricingVariant.Immediate).toBe(AppraisalPricingVariant.Immediate);
  });
});

describe('fetchPrice', () => {
  const mockPricerItem = {
    date: '2024-01-01',
    market: { id: 2, name: 'Jita 4-4' },
    buyOrderCount: 10,
    buyVolume: 100,
    sellOrderCount: 5,
    sellVolume: 50,
    immediatePrices: {
      buyPrice: 100,
      splitPrice: 95,
      sellPrice: 110,
      buyPrice5DayMedian: 98,
      splitPrice5DayMedian: 93,
      sellPrice5DayMedian: 108,
      buyPrice30DayMedian: 97,
      splitPrice30DayMedian: 92,
      sellPrice30DayMedian: 107,
    },
    top5AveragePrices: {
      buyPrice: 99,
      splitPrice: 94,
      sellPrice: 109,
      buyPrice5DayMedian: 97,
      splitPrice5DayMedian: 92,
      sellPrice5DayMedian: 106,
      buyPrice30DayMedian: 96,
      splitPrice30DayMedian: 91,
      sellPrice30DayMedian: 105,
    },
    itemType: { eid: 34, name: 'Tritanium', volume: 0.01, packagedVolume: 0.01 },
  };

  beforeEach(() => {
    mockFetch.mockImplementation(() => Promise.resolve({
      ok: true,
      json: () => Promise.resolve(mockPricerItem),
    }));
  });

  it('should fetch price successfully', async () => {
    const result = await fetchPrice(34, 2);
    expect(result).toEqual(mockPricerItem);
    expect(mockFetch).toHaveBeenCalledWith(
      'https://janice.e-351.com/api/rest/v2/pricer/34?market=2',
      expect.any(Object)
    );
  });

  it('should throw error for invalid type_id', async () => {
    await expect(fetchPrice(0)).rejects.toThrow('Invalid type_id: must be a positive number');
    await expect(fetchPrice(-1)).rejects.toThrow('Invalid type_id: must be a positive number');
    await expect(fetchPrice('34' as any)).rejects.toThrow('Invalid type_id: must be a positive number');
  });

  it('should throw error for invalid market_id', async () => {
    await expect(fetchPrice(34, 0)).rejects.toThrow('Invalid market_id: must be a positive number');
  });

  it('should handle API errors', async () => {
    mockFetch.mockImplementation(() => Promise.resolve({
      ok: false,
      status: 404,
      statusText: 'Not Found',
    }));

    await expect(fetchPrice(34)).rejects.toThrow('API request failed: 404 Not Found');
  });

  it('should handle network errors', async () => {
    mockFetch.mockImplementation(() => Promise.reject(new Error('Network error')));

    await expect(fetchPrice(34)).rejects.toThrow('Network error');
  });
});

describe('fetchPrices', () => {
  const mockPricerItems = [
    {
      date: '2024-01-01',
      market: { id: 2, name: 'Jita 4-4' },
      buyOrderCount: 10,
      buyVolume: 100,
      sellOrderCount: 5,
      sellVolume: 50,
      immediatePrices: {
        buyPrice: 100,
        splitPrice: 95,
        sellPrice: 110,
        buyPrice5DayMedian: 98,
        splitPrice5DayMedian: 93,
        sellPrice5DayMedian: 108,
        buyPrice30DayMedian: 97,
        splitPrice30DayMedian: 92,
        sellPrice30DayMedian: 107,
      },
      top5AveragePrices: {
        buyPrice: 99,
        splitPrice: 94,
        sellPrice: 109,
        buyPrice5DayMedian: 97,
        splitPrice5DayMedian: 92,
        sellPrice5DayMedian: 106,
        buyPrice30DayMedian: 96,
        splitPrice30DayMedian: 91,
        sellPrice30DayMedian: 105,
      },
      itemType: { eid: 34, name: 'Tritanium', volume: 0.01, packagedVolume: 0.01 },
    },
  ];

  beforeEach(() => {
    mockFetch.mockImplementation(() => Promise.resolve({
      ok: true,
      json: () => Promise.resolve(mockPricerItems),
    }));
  });

  it('should fetch prices successfully', async () => {
    const result = await fetchPrices([34, 35], 2);
    expect(result).toEqual(mockPricerItems);
    expect(mockFetch).toHaveBeenCalledWith(
      'https://janice.e-351.com/api/rest/v2/pricer?market=2',
      expect.objectContaining({
        method: 'POST',
        body: '34\n35',
      })
    );
  });

  it('should throw error for invalid type_ids', async () => {
    await expect(fetchPrices([])).rejects.toThrow('Invalid type_ids: must be a non-empty array of positive numbers');
    await expect(fetchPrices([0, 1])).rejects.toThrow('Invalid type_ids: must be a non-empty array of positive numbers');
    await expect(fetchPrices(['34'] as any)).rejects.toThrow('Invalid type_ids: must be a non-empty array of positive numbers');
  });
});

describe('fetchAppraisal', () => {
  const mockAppraisal = {
    id: 123,
    created: '2024-01-01T00:00:00Z',
    expires: '2024-01-02T00:00:00Z',
    datasetTime: '2024-01-01T00:00:00Z',
    code: 'ABC123',
    designation: AppraisalDesignation.Appraisal,
    pricing: AppraisalPricing.Buy,
    pricingVariant: AppraisalPricingVariant.Immediate,
    pricePercentage: 1,
    isCompactized: true,
    failures: '',
    market: { id: 2, name: 'Jita 4-4' },
    totalVolume: 100,
    totalPackagedVolume: 100,
    effectivePrices: { totalBuyPrice: 1000, totalSplitPrice: 950, totalSellPrice: 1100 },
    immediatePrices: { totalBuyPrice: 1000, totalSplitPrice: 950, totalSellPrice: 1100 },
    top5AveragePrices: { totalBuyPrice: 990, totalSplitPrice: 940, totalSellPrice: 1090 },
    items: [],
  };

  beforeEach(() => {
    mockFetch.mockImplementation(() => Promise.resolve({
      ok: true,
      json: () => Promise.resolve(mockAppraisal),
    }));
  });

  it('should fetch appraisal successfully', async () => {
    const result = await fetchAppraisal('ABC123');
    expect(result).toEqual(mockAppraisal);
    expect(mockFetch).toHaveBeenCalledWith(
      'https://janice.e-351.com/api/rest/v2/appraisal/ABC123',
      expect.any(Object)
    );
  });

  it('should throw error for invalid code', async () => {
    await expect(fetchAppraisal('')).rejects.toThrow('Invalid code: must be a non-empty string');
    await expect(fetchAppraisal('   ')).rejects.toThrow('Invalid code: must be a non-empty string');
  });
});

describe('appraiseItems', () => {
  const mockAppraisal = {
    id: 123,
    created: '2024-01-01T00:00:00Z',
    expires: '2024-01-02T00:00:00Z',
    datasetTime: '2024-01-01T00:00:00Z',
    code: 'ABC123',
    designation: AppraisalDesignation.Appraisal,
    pricing: AppraisalPricing.Buy,
    pricingVariant: AppraisalPricingVariant.Immediate,
    pricePercentage: 1,
    isCompactized: true,
    failures: '',
    market: { id: 2, name: 'Jita 4-4' },
    totalVolume: 100,
    totalPackagedVolume: 100,
    effectivePrices: { totalBuyPrice: 1000, totalSplitPrice: 950, totalSellPrice: 1100 },
    immediatePrices: { totalBuyPrice: 1000, totalSplitPrice: 950, totalSellPrice: 1100 },
    top5AveragePrices: { totalBuyPrice: 990, totalSplitPrice: 940, totalSellPrice: 1090 },
    items: [],
  };

  beforeEach(() => {
    mockFetch.mockImplementation(() => Promise.resolve({
      ok: true,
      json: () => Promise.resolve(mockAppraisal),
    }));
  });

  it('should appraise items successfully', async () => {
    const text = 'Tritanium 100\nPyerite 50';
    const result = await appraiseItems(text, 2);
    expect(result).toEqual(mockAppraisal);
    expect(mockFetch).toHaveBeenCalledWith(
      'https://janice.e-351.com/api/rest/v2/appraisal?market=2&persist=true&compactize=true&pricePercentage=1',
      expect.objectContaining({
        method: 'POST',
        body: text,
      })
    );
  });

  it('should throw error for invalid text', async () => {
    await expect(appraiseItems('')).rejects.toThrow('Invalid text: must be a non-empty string');
    await expect(appraiseItems('   ')).rejects.toThrow('Invalid text: must be a non-empty string');
  });
});

// Note: Caching tests would require either exposing the cache or using a different approach
// For now, we've tested the basic functionality. In a production environment,
// you might want to add integration tests that verify caching behavior.
