import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import {
  evaluateCustomerLifecycle,
  type CustomerLifecycleInput,
} from '../src/lifecycle';

const NOW = new Date('2026-08-13T12:00:00.000Z');
const ACCOUNT_ID = '10000000-0000-4000-8000-000000000001';
const DEAL_ID = '20000000-0000-4000-8000-000000000001';

function input(overrides: Partial<CustomerLifecycleInput> = {}): CustomerLifecycleInput {
  return {
    accountId: ACCOUNT_ID,
    deals: [],
    requests: [],
    allocations: [],
    contracts: [],
    obligations: [],
    lastActivityAt: NOW,
    ...overrides,
  };
}

describe('customer lifecycle rules', () => {
  it('uses allocation state and a fixed clock rather than treating a hold as deployment', () => {
    const base = {
      id: '30000000-0000-4000-8000-000000000001',
      demandDealId: DEAL_ID,
      gpuHours: 1_000,
      startsAt: new Date('2026-08-01T00:00:00.000Z'),
      endsAt: new Date('2026-09-01T00:00:00.000Z'),
    };
    const held = evaluateCustomerLifecycle(input({ allocations: [{ ...base, status: 'planned', holdExpiresAt: new Date('2026-08-14T00:00:00.000Z') }] }), NOW);
    const deployed = evaluateCustomerLifecycle(input({ allocations: [{ ...base, status: 'active' }] }), NOW);

    assert.equal(held.relationshipState, 'prospect');
    assert.equal(held.heldCapacityGpuHours, 1_000);
    assert.equal(deployed.relationshipState, 'deployed');
  });

  it('suppresses ending-capacity risk when a future reservation covers the same deal', () => {
    const current = {
      id: '30000000-0000-4000-8000-000000000001',
      demandDealId: DEAL_ID,
      status: 'active' as const,
      gpuHours: 1_000,
      startsAt: new Date('2026-07-01T00:00:00.000Z'),
      endsAt: new Date('2026-08-20T00:00:00.000Z'),
    };
    const future = {
      ...current,
      id: '30000000-0000-4000-8000-000000000002',
      status: 'committed' as const,
      startsAt: new Date('2026-08-20T00:00:00.000Z'),
      endsAt: new Date('2026-10-01T00:00:00.000Z'),
    };
    const result = evaluateCustomerLifecycle(input({ allocations: [current, future] }), NOW);

    assert.equal(result.signals.some((signal) => signal.code === 'allocation_ending_uncovered'), false);
  });

  it('describes missing activity as stale CRM evidence, never customer disengagement', () => {
    const result = evaluateCustomerLifecycle(input({ lastActivityAt: null }), NOW);
    const stale = result.signals.find((signal) => signal.code === 'crm_evidence_stale_90');

    assert.match(stale?.explanation ?? '', /CRM evidence is stale/);
    assert.match(stale?.explanation ?? '', /does not establish customer disengagement/);
  });

  it('orders explainable weighted signals deterministically', () => {
    const result = evaluateCustomerLifecycle(input({
      deals: [{ id: DEAL_ID, stage: 'expansion', productLine: 'compute_reserved', msaExecuted: true, lastActivityAt: NOW }],
      requests: [{ id: '40000000-0000-4000-8000-000000000001', demandDealId: DEAL_ID, totalGpuHours: 500 }],
      contracts: [{ id: '50000000-0000-4000-8000-000000000001', status: 'executed', isAutoRenew: true, noticeDays: 30, expiresAt: new Date('2026-09-01T00:00:00.000Z') }],
    }), NOW);

    assert.deepEqual(result.signals.map((signal) => signal.weight), [...result.signals.map((signal) => signal.weight)].sort((a, b) => b - a));
    assert.ok(result.signals.every((signal) => signal.explanation && signal.sourceRefs.length));
    assert.equal(result.rulesetVersion, 'growth-r1-2026-08-13');
    assert.equal(result.computedAt, NOW.toISOString());
  });

  it('labels allocation volume only as sold or held capacity', () => {
    const result = evaluateCustomerLifecycle(input(), NOW);
    const serialized = JSON.stringify(result);
    assert.doesNotMatch(serialized, /workload utilization|customer utilization/i);
    assert.ok('soldCapacityGpuHours' in result);
    assert.ok('heldCapacityGpuHours' in result);
  });
});
