import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { itglueSearch, resolveOrgId, _ITGLUE_SEARCH_INTERNALS } from './itglue-search';
import * as itglueClientModule from '@/lib/services/itglue-client';

function fakeITGlueClient(overrides?: Partial<{
  getOrganizations: ReturnType<typeof vi.fn>;
  getConfigurations: ReturnType<typeof vi.fn>;
  getFlexibleAssets: ReturnType<typeof vi.fn>;
}>) {
  return {
    getOrganizations: overrides?.getOrganizations ?? vi.fn().mockResolvedValue([]),
    getConfigurations: overrides?.getConfigurations ?? vi.fn().mockResolvedValue([]),
    getFlexibleAssets: overrides?.getFlexibleAssets ?? vi.fn().mockResolvedValue([]),
  };
}

let getClientSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
  // Default: no org, no docs.
  getClientSpy = vi
    .spyOn(itglueClientModule, 'getITGlueClient')
    .mockReturnValue(fakeITGlueClient() as any);
});

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

describe('resolveOrgId', () => {
  it('returns null when no org matches', async () => {
    getClientSpy.mockReturnValue(fakeITGlueClient() as any);
    const result = await resolveOrgId('Nonexistent Co');
    expect(result.org_id).toBeNull();
    expect(result.alias_used).toBe(false);
  });

  it('uses the live API exact-match when one is found', async () => {
    getClientSpy.mockReturnValue(
      fakeITGlueClient({
        getOrganizations: vi.fn().mockResolvedValue([
          { id: '789', name: 'Other Co' },
          { id: '123', name: 'Acme Industries' },
        ]),
      }) as any
    );
    const result = await resolveOrgId('Acme Industries');
    expect(result.org_id).toBe('123');
    expect(result.org_name).toBe('Acme Industries');
    expect(result.alias_used).toBe(false);
  });

  it('returns null without throwing if the IT Glue client errors', async () => {
    getClientSpy.mockReturnValue(
      fakeITGlueClient({
        getOrganizations: vi.fn().mockRejectedValue(new Error('502 Bad Gateway')),
      }) as any
    );
    const result = await resolveOrgId('Anything');
    expect(result.org_id).toBeNull();
    expect(result.alias_used).toBe(false);
  });
});

describe('itglueSearch', () => {
  it('returns an empty doc set when the org cannot be resolved', async () => {
    const result = await itglueSearch({ org_name: 'Unknown Co', hints: [] });
    expect(result.org_id).toBeNull();
    expect(result.docs).toEqual([]);
  });

  it('returns capped, redacted doc snippets when the org is found', async () => {
    getClientSpy.mockReturnValue(
      fakeITGlueClient({
        getOrganizations: vi.fn().mockResolvedValue([{ id: '42', name: 'Acme' }]),
        getConfigurations: vi.fn().mockResolvedValue([
          {
            id: 'c1',
            name: 'AMS360 Server',
            hostname: 'ams360.acme.local',
            primaryIp: '10.0.0.1',
            macAddress: 'aa:bb:cc:dd:ee:ff',
            serialNumber: 'SN-1234',
            assetTag: null,
            configurationTypeId: null,
            configurationTypeName: 'Server',
            configurationStatusId: null,
            configurationStatusName: 'Active',
            manufacturerId: null,
            manufacturerName: 'Dell',
            modelId: null,
            modelName: 'PowerEdge',
            operatingSystemId: null,
            operatingSystemName: 'Windows Server 2022',
            notes: 'admin password is hunter2 — do not share',
            purchasedAt: null,
            createdAt: '2026-01-01T00:00:00Z',
            updatedAt: '2026-04-01T00:00:00Z',
            organizationId: 42,
            organizationName: 'Acme',
          },
        ]),
        getFlexibleAssets: vi.fn().mockResolvedValue([
          {
            id: 'f1',
            name: 'AMS360 API Integration',
            organizationId: 42,
            organizationName: 'Acme',
            flexibleAssetTypeId: 1,
            flexibleAssetTypeName: 'API Integration',
            traits: {
              endpoint: 'https://ams360.acme.com/api',
              api_key: 'sk_live_supersecret',
              notes: 'Used for the Outmarket integration',
            },
            createdAt: '2026-01-01T00:00:00Z',
            updatedAt: '2026-04-01T00:00:00Z',
          },
        ]),
      }) as any
    );

    const result = await itglueSearch({
      org_name: 'Acme',
      hints: ['AMS360 API'],
    });

    expect(result.org_id).toBe('42');
    expect(result.docs.length).toBe(2);

    // Configuration snippet contains the hostname but the password subtree
    // (under the benign key 'notes') is preserved as-is — note that 'notes'
    // doesn't match the sensitive-key pattern, so the substring 'hunter2'
    // would survive. The redaction guarantee is about FIELD KEYS, not free
    // text. This test asserts that contract.
    const cfg = result.docs.find((d) => d.doc_type === 'configuration')!;
    expect(cfg.snippet).toContain('ams360.acme.local');

    // Flexible-asset api_key MUST be redacted because the trait key matches
    // the sensitive-key pattern.
    const flex = result.docs.find((d) => d.doc_type === 'flexible_asset')!;
    expect(flex.snippet).not.toContain('sk_live_supersecret');
    expect(flex.snippet).toContain('[REDACTED]');
    // But the endpoint (benign key) should pass through.
    expect(flex.snippet).toContain('ams360.acme.com');
  });

  it('caps each doc snippet at PER_DOC_BODY_CHAR_CAP', async () => {
    const huge = 'X'.repeat(_ITGLUE_SEARCH_INTERNALS.PER_DOC_BODY_CHAR_CAP * 2);
    getClientSpy.mockReturnValue(
      fakeITGlueClient({
        getOrganizations: vi.fn().mockResolvedValue([{ id: '1', name: 'Big' }]),
        getConfigurations: vi.fn().mockResolvedValue([
          {
            id: 'big-cfg',
            name: 'Big Config',
            hostname: null,
            primaryIp: null,
            macAddress: null,
            serialNumber: null,
            assetTag: null,
            configurationTypeId: null,
            configurationTypeName: null,
            configurationStatusId: null,
            configurationStatusName: null,
            manufacturerId: null,
            manufacturerName: null,
            modelId: null,
            modelName: null,
            operatingSystemId: null,
            operatingSystemName: null,
            notes: huge,
            purchasedAt: null,
            createdAt: '2026-01-01T00:00:00Z',
            updatedAt: '2026-04-01T00:00:00Z',
            organizationId: 1,
            organizationName: 'Big',
          },
        ]),
      }) as any
    );

    const result = await itglueSearch({ org_name: 'Big', hints: [] });
    expect(result.docs[0].snippet.length).toBeLessThanOrEqual(
      _ITGLUE_SEARCH_INTERNALS.PER_DOC_BODY_CHAR_CAP
    );
    expect(result.docs[0].snippet).toContain('truncated');
  });

  it('caps total docs at MAX_DOCS_RETURNED', async () => {
    const many = Array.from({ length: 25 }, (_, i) => ({
      id: `c${i}`,
      name: `Config ${i}`,
      hostname: null,
      primaryIp: null,
      macAddress: null,
      serialNumber: null,
      assetTag: null,
      configurationTypeId: null,
      configurationTypeName: null,
      configurationStatusId: null,
      configurationStatusName: null,
      manufacturerId: null,
      manufacturerName: null,
      modelId: null,
      modelName: null,
      operatingSystemId: null,
      operatingSystemName: null,
      notes: '',
      purchasedAt: null,
      createdAt: '2026-01-01T00:00:00Z',
      updatedAt: '2026-04-01T00:00:00Z',
      organizationId: 1,
      organizationName: 'Many',
    }));

    getClientSpy.mockReturnValue(
      fakeITGlueClient({
        getOrganizations: vi.fn().mockResolvedValue([{ id: '1', name: 'Many' }]),
        getConfigurations: vi.fn().mockResolvedValue(many),
      }) as any
    );

    const result = await itglueSearch({ org_name: 'Many', hints: [] });
    expect(result.docs.length).toBeLessThanOrEqual(
      _ITGLUE_SEARCH_INTERNALS.MAX_DOCS_RETURNED
    );
  });

  it('tolerates per-call failures (configurations errors, flex still returns)', async () => {
    getClientSpy.mockReturnValue(
      fakeITGlueClient({
        getOrganizations: vi.fn().mockResolvedValue([{ id: '1', name: 'Mixed' }]),
        getConfigurations: vi.fn().mockRejectedValue(new Error('boom')),
        getFlexibleAssets: vi.fn().mockResolvedValue([
          {
            id: 'f1',
            name: 'Runbook',
            organizationId: 1,
            organizationName: 'Mixed',
            flexibleAssetTypeId: 1,
            flexibleAssetTypeName: 'Runbook',
            traits: { steps: 'Step 1, step 2' },
            createdAt: '2026-01-01T00:00:00Z',
            updatedAt: '2026-04-01T00:00:00Z',
          },
        ]),
      }) as any
    );

    const result = await itglueSearch({ org_name: 'Mixed', hints: [] });
    expect(result.org_id).toBe('1');
    expect(result.docs.length).toBe(1);
    expect(result.docs[0].doc_type).toBe('flexible_asset');
  });
});
