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

import { gqlFetcherFactory } from '../fetcher';
import type { GraphQLQuery } from '../graphql';
import {
  generateRandUTF16Chars,
  SPAN_ID_BYTES,
  TRACE_ID_BYTES,
  TraceReporter,
} from '../utils';

const query: GraphQLQuery = {
  id: 'query',
  query: 'query { field }',
  operationName: 'query',
  definitionName: 'query',
};

let fetch: Mock;
describe('GraphQL fetcher', () => {
  beforeEach(() => {
    fetch = vi.fn(() =>
      Promise.resolve(
        new Response(JSON.stringify({ data: { field: 1 } }), {
          headers: {
            'content-type': 'application/json',
          },
        })
      )
    );
    vi.stubGlobal('fetch', fetch);
  });

  afterEach(() => {
    fetch.mockReset();
  });

  const gql = gqlFetcherFactory('https://example.com/graphql');

  it('should send POST request to given endpoint', async () => {
    await gql(
      // @ts-expect-error variables is actually optional
      { query }
    );

    expect(fetch).toBeCalledTimes(1);
    expect(fetch.mock.lastCall[0]).toBe('https://example.com/graphql');
    const ctx = fetch.mock.lastCall[1] as RequestInit;
    expect(ctx.method).toBe('POST');
  });

  it('should send with correct graphql JSON body', async () => {
    await gql({
      query,
      // @ts-expect-error forgive the fake variables
      variables: { a: 1, b: '2', c: { d: false } },
    });

    expect(fetch.mock.lastCall[1]).toEqual(
      expect.objectContaining({
        body: '{"query":"query { field }","variables":{"a":1,"b":"2","c":{"d":false}},"operationName":"query"}',
        headers: expect.objectContaining({
          'content-type': 'application/json',
          'x-definition-name': 'query',
          'x-operation-name': 'query',
          'x-request-id': expect.any(String),
        }),
        method: 'POST',
      })
    );
  });

  it('should correctly ignore nil variables', async () => {
    await gql({
      query,
      // @ts-expect-error forgive the fake variables
      variables: { a: false, b: null, c: undefined },
    });

    expect(fetch.mock.lastCall[1].body).toMatchInlineSnapshot(
      `"{"query":"query { field }","variables":{"a":false,"b":null},"operationName":"query"}"`
    );

    await gql({
      query,
      // @ts-expect-error forgive the fake variables
      variables: { a: false, b: null, c: undefined },
      keepNilVariables: false,
    });

    expect(fetch.mock.lastCall[1].body).toMatchInlineSnapshot(
      `"{"query":"query { field }","variables":{"a":false},"operationName":"query"}"`
    );
  });

  it('should correct handle graphql error', async () => {
    fetch.mockResolvedValue(
      new Response(
        JSON.stringify({
          data: null,
          errors: [{ message: 'error', path: ['field'] }],
        }),
        {
          headers: {
            'content-type': 'application/json',
          },
          status: 400,
        }
      )
    );

    await expect(gql({ query, variables: void 0 })).rejects
      .toMatchInlineSnapshot(`
      [
        [GraphQLError: error],
      ]
    `);
  });
});

describe('Trace Reporter', () => {
  const startTime = new Date().toISOString();
  const traceId = generateRandUTF16Chars(TRACE_ID_BYTES);
  const spanId = generateRandUTF16Chars(SPAN_ID_BYTES);
  const requestId = nanoid();

  it('spanId, traceId should be right format', () => {
    expect(
      new RegExp(`^[0-9a-f]{${SPAN_ID_BYTES * 2}}$`).test(
        generateRandUTF16Chars(SPAN_ID_BYTES)
      )
    ).toBe(true);
    expect(
      new RegExp(`^[0-9a-f]{${TRACE_ID_BYTES * 2}}$`).test(
        generateRandUTF16Chars(TRACE_ID_BYTES)
      )
    ).toBe(true);
  });

  it('test createTraceSpan', () => {
    const traceSpan = TraceReporter.createTraceSpan(
      traceId,
      spanId,
      startTime,
      { requestId }
    );
    expect(traceSpan.startTime).toBe(startTime);
    expect(
      traceSpan.name ===
        `projects/{GCP_PROJECT_ID}/traces/${traceId}/spans/${spanId}`
    ).toBe(true);
    expect(traceSpan.spanId).toBe(spanId);
    expect(traceSpan.attributes.attributeMap.requestId?.stringValue.value).toBe(
      requestId
    );
  });
});
