import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import {
  buildCallProviderOptions,
  isPlainObject,
  mergeModelLevel,
  pinQuantization,
  stripDenyListed,
} from './provider_options';

describe('isPlainObject', () => {
  it('returns true for plain objects', () => {
    expect(isPlainObject({})).toBe(true);
    expect(isPlainObject({ a: 1 })).toBe(true);
  });

  it('returns false for null', () => {
    expect(isPlainObject(null)).toBe(false);
  });

  it('returns false for arrays', () => {
    expect(isPlainObject([])).toBe(false);
    expect(isPlainObject([1, 2, 3])).toBe(false);
  });

  it('returns false for primitives', () => {
    expect(isPlainObject(0)).toBe(false);
    expect(isPlainObject('')).toBe(false);
    expect(isPlainObject(false)).toBe(false);
    expect(isPlainObject(undefined)).toBe(false);
  });
});

describe('mergeModelLevel', () => {
  it('returns undefined when both sides are absent', () => {
    expect(mergeModelLevel(undefined, undefined)).toBeUndefined();
  });

  it('returns provider-level when model-level is absent', () => {
    expect(
      mergeModelLevel({ provider: { allow_fallbacks: false } }, undefined),
    ).toEqual({ provider: { allow_fallbacks: false } });
  });

  it('returns model-level when provider-level is absent', () => {
    expect(
      mergeModelLevel(undefined, { provider: { quantizations: ['fp8'] } }),
    ).toEqual({ provider: { quantizations: ['fp8'] } });
  });

  it('depth-2 merges nested plain objects with model winning on conflict', () => {
    expect(
      mergeModelLevel(
        { provider: { allow_fallbacks: false, data_collection: 'deny' } },
        { provider: { quantizations: ['fp8'], data_collection: 'allow' } },
      ),
    ).toEqual({
      provider: {
        allow_fallbacks: false,
        data_collection: 'allow',
        quantizations: ['fp8'],
      },
    });
  });

  it('replaces arrays wholesale (no concat)', () => {
    expect(
      mergeModelLevel(
        { provider: { only: ['Together', 'Fireworks'] } },
        { provider: { only: ['Hyperbolic'] } },
      ),
    ).toEqual({ provider: { only: ['Hyperbolic'] } });
  });

  it('does not recurse below depth 2 (depth-3 values replace wholesale)', () => {
    expect(
      mergeModelLevel(
        { provider: { max_price: { prompt: 1, completion: 5 } } },
        { provider: { max_price: { prompt: 2 } } },
      ),
    ).toEqual({ provider: { max_price: { prompt: 2 } } });
  });

  it('treats undefined sub-values as absent (other side wins)', () => {
    expect(
      mergeModelLevel(
        { provider: { allow_fallbacks: false } },
        { provider: { allow_fallbacks: undefined } },
      ),
    ).toEqual({ provider: { allow_fallbacks: false } });
  });

  it('treats null as a real value that replaces', () => {
    expect(
      mergeModelLevel(
        { provider: { allow_fallbacks: false } },
        { provider: { allow_fallbacks: null } },
      ),
    ).toEqual({ provider: { allow_fallbacks: null } });
  });

  it('prunes top-level keys whose value is the empty object', () => {
    expect(mergeModelLevel({ provider: {} }, { provider: {} })).toBeUndefined();
  });

  it('prunes empty top-level keys but preserves non-empty siblings', () => {
    expect(
      mergeModelLevel(
        { provider: { allow_fallbacks: false }, ignored: {} },
        undefined,
      ),
    ).toEqual({ provider: { allow_fallbacks: false } });
  });

  it('replaces wholesale when one side is an object and the other is a primitive', () => {
    expect(
      mergeModelLevel({ provider: { only: ['x'] } }, { provider: 'forbidden' }),
    ).toEqual({ provider: 'forbidden' });
  });
});

describe('stripDenyListed', () => {
  let errorSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
  });

  afterEach(() => {
    errorSpy.mockRestore();
  });

  it('returns undefined for undefined input', () => {
    expect(stripDenyListed(undefined)).toBeUndefined();
  });

  it('strips top-level body-overwrite keys with an error log', () => {
    expect(
      stripDenyListed({ provider: { quantizations: ['fp8'] }, model: 'evil' }),
    ).toEqual({ provider: { quantizations: ['fp8'] } });
    expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("'model'"));
  });

  it('strips SDK-reserved keys with an error log', () => {
    expect(
      stripDenyListed({ provider: {}, reasoningEffort: 'high' }),
    ).toBeUndefined();
    expect(errorSpy).toHaveBeenCalledWith(
      expect.stringContaining("'reasoningEffort'"),
    );
  });

  it('strips deny-listed keys nested one level deep', () => {
    expect(
      stripDenyListed({
        openrouter: { provider: { quantizations: ['fp8'] }, model: 'evil' },
      }),
    ).toEqual({
      openrouter: { provider: { quantizations: ['fp8'] } },
    });
    expect(errorSpy).toHaveBeenCalledWith(
      expect.stringContaining("'openrouter.model'"),
    );
  });

  it('preserves non-deny-listed keys verbatim', () => {
    expect(
      stripDenyListed({ provider: { quantizations: ['fp8'], order: ['T'] } }),
    ).toEqual({ provider: { quantizations: ['fp8'], order: ['T'] } });
  });
});

describe('pinQuantization', () => {
  it('rewrites a multi-element quantizations array to a single element', () => {
    expect(
      pinQuantization(
        {
          provider: { quantizations: ['fp8', 'fp4'], data_collection: 'deny' },
        },
        'fp4',
      ),
    ).toEqual({
      provider: { quantizations: ['fp4'], data_collection: 'deny' },
    });
  });

  it('creates the provider wrapper when input is undefined', () => {
    expect(pinQuantization(undefined, 'fp4')).toEqual({
      provider: { quantizations: ['fp4'] },
    });
  });

  it('creates the provider wrapper when input has no provider key', () => {
    expect(pinQuantization({ openrouter: { only: ['T'] } }, 'fp8')).toEqual({
      openrouter: { only: ['T'] },
      provider: { quantizations: ['fp8'] },
    });
  });

  it('does not mutate its inputs', () => {
    const input = {
      provider: { quantizations: ['fp8', 'fp4'], data_collection: 'deny' },
    };
    const snapshot = JSON.parse(JSON.stringify(input));
    pinQuantization(input, 'fp4');
    expect(input).toEqual(snapshot);
  });

  it('replaces a non-object provider value with a fresh wrapper', () => {
    expect(pinQuantization({ provider: 'forbidden' }, 'fp4')).toEqual({
      provider: { quantizations: ['fp4'] },
    });
  });

  it('preserves sibling provider fields and adds quantizations', () => {
    expect(
      pinQuantization(
        { provider: { allow_fallbacks: false, only: ['Hyperbolic'] } },
        'fp8',
      ),
    ).toEqual({
      provider: {
        allow_fallbacks: false,
        only: ['Hyperbolic'],
        quantizations: ['fp8'],
      },
    });
  });
});

describe('buildCallProviderOptions', () => {
  it('returns undefined when no providerOptions are configured', () => {
    expect(
      buildCallProviderOptions({ providerName: 'openrouter' }),
    ).toBeUndefined();
  });

  it('returns undefined when providerOptions is the empty object', () => {
    expect(
      buildCallProviderOptions({
        providerName: 'openrouter',
        providerOptions: {},
      }),
    ).toBeUndefined();
  });

  it('namespaces under the kebab providerName', () => {
    expect(
      buildCallProviderOptions({
        providerName: 'vercel-gateway',
        providerOptions: { provider: { quantizations: ['fp8'] } },
      }),
    ).toEqual({
      'vercel-gateway': { provider: { quantizations: ['fp8'] } },
    });
  });

  it('applies the deny-list strip before namespacing', () => {
    const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
    try {
      expect(
        buildCallProviderOptions({
          providerName: 'openrouter',
          providerOptions: {
            provider: { quantizations: ['fp8'] },
            model: 'evil',
            max_tokens: 999_999,
          },
        }),
      ).toEqual({
        openrouter: { provider: { quantizations: ['fp8'] } },
      });
      expect(errorSpy).toHaveBeenCalled();
    } finally {
      errorSpy.mockRestore();
    }
  });
});

// End-to-end shape contract: the merge → pin → namespace pipeline must
// produce exactly the body fields OpenRouter expects when a Tale model ref
// like `openrouter:z-ai/glm-5.1@fp8` is resolved. The `@fp8` suffix lives
// only inside Tale; on the wire the `model` field is bare and the variant
// rides in `provider.quantizations` as a single-element array.
describe('quantization wire-shape pipeline', () => {
  it('produces the exact namespaced body fields OpenRouter expects', () => {
    // Stand-in for the merge step in resolveModelData: provider-level
    // defaults + model-level passthrough, post-merge.
    const merged = mergeModelLevel(
      { provider: { allow_fallbacks: false, data_collection: 'deny' } },
      { provider: { quantizations: ['fp8', 'fp4'] } },
    );

    // resolveModelData pins the user-selected variant before returning.
    const pinned = pinQuantization(merged, 'fp4');

    // The call site spreads buildCallProviderOptions into streamText.
    expect(
      buildCallProviderOptions({
        providerName: 'openrouter',
        providerOptions: pinned,
      }),
    ).toEqual({
      openrouter: {
        provider: {
          allow_fallbacks: false,
          data_collection: 'deny',
          quantizations: ['fp4'],
        },
      },
    });
  });

  it('keeps non-`provider` namespaced sub-objects intact alongside the pin', () => {
    const merged = mergeModelLevel(
      { openrouter: { only: ['Hyperbolic'] } },
      { provider: { quantizations: ['fp8'] } },
    );
    const pinned = pinQuantization(merged, 'fp8');
    expect(
      buildCallProviderOptions({
        providerName: 'openrouter',
        providerOptions: pinned,
      }),
    ).toEqual({
      openrouter: {
        openrouter: { only: ['Hyperbolic'] },
        provider: { quantizations: ['fp8'] },
      },
    });
  });
});
