import { describe, it, expect } from 'vitest';
import { redact, isSensitiveKey, REDACTED_VALUE } from './itglue-redact';

describe('isSensitiveKey', () => {
  it.each([
    ['password', true],
    ['Password', true],
    ['PASSWORD', true],
    ['user_password', true],
    ['secret', true],
    ['client_secret', true],
    ['apiKey', true],
    ['api_key', true],
    ['api-key', true],
    ['API_KEY', true],
    ['authToken', true],
    ['accessToken', true],
    ['credentials', true],
    ['masterKey', true],
    ['privateKey', true],
    ['name', false],
    ['email', false],
    ['id', false],
    ['title', false],
    ['hostname', false],
    ['username', false], // intentional: username alone isn't a credential
  ])('%s -> %s', (key, expected) => {
    expect(isSensitiveKey(key)).toBe(expected);
  });
});

describe('redact', () => {
  it('redacts top-level sensitive keys', () => {
    const input = { id: 'abc', password: 'hunter2', name: 'wifi' };
    expect(redact(input)).toEqual({
      id: 'abc',
      password: REDACTED_VALUE,
      name: 'wifi',
    });
  });

  it('redacts nested object credentials', () => {
    const input = {
      id: 'abc',
      traits: {
        username: 'svc-account',
        password: 'p@ss',
        api_key: 'ak_123',
      },
    };
    expect(redact(input)).toEqual({
      id: 'abc',
      traits: {
        username: 'svc-account',
        password: REDACTED_VALUE,
        api_key: REDACTED_VALUE,
      },
    });
  });

  it('redacts inside arrays of objects', () => {
    const input = {
      passwords: [
        { id: 1, name: 'admin', password: 'topsecret' },
        { id: 2, name: 'svc', password: 'alsotopsecret' },
      ],
    };
    // The outer key "passwords" matches → entire array is redacted.
    expect(redact(input)).toEqual({ passwords: REDACTED_VALUE });
  });

  it('redacts per-item secrets when the array key is benign', () => {
    const input = {
      accounts: [
        { id: 1, name: 'admin', password: 'topsecret' },
        { id: 2, name: 'svc', api_key: 'ak_456' },
      ],
    };
    expect(redact(input)).toEqual({
      accounts: [
        { id: 1, name: 'admin', password: REDACTED_VALUE },
        { id: 2, name: 'svc', api_key: REDACTED_VALUE },
      ],
    });
  });

  it('redacts whole subtree when key matches even if value is an object', () => {
    const input = {
      org_id: 7,
      auth: {
        type: 'oauth',
        client_id: 'cid',
        client_secret: 'shouldbehidden',
        nested: { tokens: { access: 'a', refresh: 'r' } },
      },
    };
    // Note: "auth" itself does NOT match the pattern, so it's recursed into.
    // Inside, client_id is benign, client_secret matches, nested.tokens matches.
    expect(redact(input)).toEqual({
      org_id: 7,
      auth: {
        type: 'oauth',
        client_id: 'cid',
        client_secret: REDACTED_VALUE,
        nested: { tokens: REDACTED_VALUE },
      },
    });
  });

  it('preserves non-sensitive values of all primitive types', () => {
    const input = {
      id: 'abc',
      count: 42,
      enabled: true,
      ratio: 0.5,
      tag: null,
      missing: undefined,
    };
    expect(redact(input)).toEqual(input);
  });

  it('does not mutate the input', () => {
    const input = {
      id: 'abc',
      password: 'hunter2',
      nested: { api_key: 'ak_123', name: 'svc' },
    };
    const snapshot = JSON.parse(JSON.stringify(input));
    redact(input);
    expect(input).toEqual(snapshot);
  });

  it('returns null/undefined unchanged on sensitive keys with null value', () => {
    expect(redact({ password: null })).toEqual({ password: null });
    expect(redact({ password: undefined })).toEqual({ password: undefined });
  });

  it('handles primitives at the root', () => {
    expect(redact('plain string')).toBe('plain string');
    expect(redact(42)).toBe(42);
    expect(redact(null)).toBe(null);
    expect(redact(undefined)).toBe(undefined);
  });

  it('handles top-level arrays', () => {
    const input = [
      { id: 1, password: 'a' },
      { id: 2, name: 'b' },
    ];
    expect(redact(input)).toEqual([
      { id: 1, password: REDACTED_VALUE },
      { id: 2, name: 'b' },
    ]);
  });

  it('does not crash on cyclic references', () => {
    const a: Record<string, unknown> = { id: 1 };
    a.self = a; // cycle
    a.password = 'secret';
    const out = redact(a) as Record<string, unknown>;
    expect(out.id).toBe(1);
    expect(out.password).toBe(REDACTED_VALUE);
    expect(out.self).toBe('[CIRCULAR]');
  });
});
