/**
 * Tests for the upstream-to-PIG mapping.
 *
 * Two things here are worth defending with tests, because getting either wrong
 * is expensive and neither would throw:
 *
 *   **Money.** Upstream sends floating-point dollars; PIG stores integer
 *   cents. Truncating instead of rounding loses a cent on values that cannot
 *   be represented exactly in binary, and a cent compounds across millions of
 *   GPU-hours.
 *
 *   **Node totals.** Prime reports on-demand price and GPU memory for the whole
 *   node. PIG compares per-GPU values, so a larger node must not look more
 *   expensive merely because it contains more GPUs.
 *
 *   **Interconnect.** The field that decides whether capacity can train or
 *   only serve. Guessing wrong in either direction loses a deal or sells a
 *   customer a cluster that cannot do the job.
 */
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import { mapListing, toCents } from '../src/map';
import type { PrimeGpuListing } from '../src/client';

const listing = (over: Partial<PrimeGpuListing> = {}): PrimeGpuListing => ({
  gpuType: 'H100_80GB',
  gpuCount: 8,
  raw: {},
  ...over,
});

describe('toCents', () => {
  it('rounds rather than truncating', () => {
    // 2.43 is 2.4299999... in binary. Truncating gives 242 and loses a cent
    // on every single listing.
    assert.equal(toCents(2.43), 243);
    assert.equal(toCents(0.47), 47);
    assert.equal(toCents(12.005), 1201);
  });

  it('passes through null and rejects nonsense', () => {
    assert.equal(toCents(null), null);
    assert.equal(toCents(undefined), null);
    assert.equal(toCents(Number.NaN), null);
    assert.equal(toCents(Number.POSITIVE_INFINITY), null);
  });

  it('handles zero, which is a real price and not a missing one', () => {
    assert.equal(toCents(0), 0);
  });
});

describe('mapListing — interconnect', () => {
  const cases: [string, string][] = [
    ['Infiniband', 'Infiniband'],
    ['InfiniBand', 'Infiniband'],
    ['INFINIBAND', 'Infiniband'],
    ['infini_band', 'Infiniband'],
    ['IB', 'Infiniband'],
    ['RoCE', 'RoCE'],
    ['roce v2', 'RoCE'],
    ['NVLink', 'NVLink'],
    ['nvl', 'NVLink'],
    ['Ethernet', 'Ethernet'],
    ['eth', 'Ethernet'],
  ];

  for (const [input, expected] of cases) {
    it(`normalises "${input}" to ${expected}`, () => {
      assert.equal(mapListing(listing({ interconnectType: input }))!.interconnectType, expected);
    });
  }

  it('maps an unrecognised fabric to Unknown, never to Ethernet', () => {
    // Assuming Ethernet would understate real capacity; assuming InfiniBand
    // would sell a training customer a cluster that cannot train. Neither
    // error is acceptable, so it stays explicitly unknown.
    assert.equal(mapListing(listing({ interconnectType: 'Omni-Path' }))!.interconnectType, 'Unknown');
    assert.equal(mapListing(listing({ interconnectType: '' }))!.interconnectType, 'Unknown');
    assert.equal(mapListing(listing({ interconnectType: undefined }))!.interconnectType, 'Unknown');
  });
});

describe('mapListing — stock', () => {
  it('treats an unrecognised stock signal as Unavailable', () => {
    // Under-promising inventory is recoverable. A seller offering capacity
    // that turns out not to exist is not.
    assert.equal(mapListing(listing({ stockStatus: 'wat' }))!.stockStatus, 'Unavailable');
    assert.equal(mapListing(listing({ stockStatus: undefined }))!.stockStatus, 'Unavailable');
  });

  it('passes known values through, case-insensitively', () => {
    assert.equal(mapListing(listing({ stockStatus: 'available' }))!.stockStatus, 'Available');
    assert.equal(mapListing(listing({ stockStatus: 'HIGH' }))!.stockStatus, 'High');
  });
});

describe('mapListing — general', () => {
  it('drops a listing with no GPU type or count — there is nothing sellable', () => {
    assert.equal(mapListing(listing({ gpuType: undefined })), null);
    assert.equal(mapListing(listing({ gpuCount: undefined })), null);
    assert.equal(mapListing(listing({ gpuCount: 0 })), null);
  });

  it('converts prices to integer cents', () => {
    const m = mapListing(
      listing({ gpuCount: 1, prices: { onDemand: 2.43, communityPrice: 0.94 } }),
    )!;
    assert.equal(m.onDemandPriceCents, 243);
    assert.equal(m.communityPriceCents, 94);
  });

  it('normalises verified DataCrunch A100 node totals to the same per-GPU values', () => {
    const oneGpuRaw = {
      provider: 'datacrunch',
      gpuType: 'A100_80GB',
      gpuCount: 1,
      gpuMemory: 80,
      prices: { onDemand: 1.79 },
    };
    const twoGpuRaw = {
      provider: 'datacrunch',
      gpuType: 'A100_80GB',
      gpuCount: 2,
      gpuMemory: 160,
      prices: { onDemand: 3.58 },
    };

    const oneGpu = mapListing({ ...oneGpuRaw, raw: oneGpuRaw })!;
    const twoGpu = mapListing({ ...twoGpuRaw, raw: twoGpuRaw })!;

    assert.equal(oneGpu.onDemandPriceCents, 179);
    assert.equal(twoGpu.onDemandPriceCents, 179);
    assert.equal(oneGpu.gpuMemoryGb, 80);
    assert.equal(twoGpu.gpuMemoryGb, 80);
    assert.deepEqual(twoGpu.raw, twoGpuRaw);
  });

  it('defaults to the secure tier unless community is stated', () => {
    // Mislabelling community capacity as secure would let it be sold against a
    // requirement it cannot meet.
    assert.equal(mapListing(listing({}))!.securityTier, 'secure_cloud');
    assert.equal(
      mapListing(listing({ security: 'community_cloud' }))!.securityTier,
      'community_cloud',
    );
  });

  it('normalises sockets and drops unknown ones rather than inventing a value', () => {
    assert.equal(mapListing(listing({ socket: 'sxm5' }))!.socket, 'SXM5');
    assert.equal(mapListing(listing({ socket: 'pcie' }))!.socket, 'PCIe');
    assert.equal(mapListing(listing({ socket: 'SXM_5' }))!.socket, 'SXM5');
    // The column is an enum; an unmapped value would fail the insert.
    assert.equal(mapListing(listing({ socket: 'weird' }))!.socket, null);
  });

  it('unwraps counts sent as objects', () => {
    const m = mapListing(listing({ vcpu: { defaultCount: 96 }, memory: 1024 }))!;
    assert.equal(m.vcpu, 96);
    assert.equal(m.memoryGb, 1024);
  });

  it('preserves the raw payload so a new upstream field is not lost', () => {
    const raw = { gpuType: 'H100_80GB', gpuCount: 8, somethingNew: 'value' };
    const m = mapListing({ ...listing(), raw })!;
    assert.equal((m.raw as Record<string, unknown>).somethingNew, 'value');
  });
});
