/**
 * @license
 * Copyright Google LLC All Rights Reserved.sonpCallbackContext
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {DOCUMENT} from '../..';
import {CSP_NONCE} from '@angular/core';
import {HttpHeaders} from '../src/headers';
import {
  JSONP_ERR_HEADERS_NOT_SUPPORTED,
  JSONP_ERR_NO_CALLBACK,
  JSONP_ERR_WRONG_METHOD,
  JSONP_ERR_WRONG_RESPONSE_TYPE,
  JsonpCallbackContext,
  JsonpClientBackend,
} from '../src/jsonp';
import {HttpRequest} from '../src/request';
import {HttpErrorResponse, HttpEventType} from '../src/response';
import {TestBed} from '@angular/core/testing';
import {toArray} from 'rxjs/operators';

import {MockDocument} from './jsonp_mock';

describe('JsonpClientBackend', () => {
  const SAMPLE_REQ = new HttpRequest<never>('JSONP', '/test');
  let home: any;
  let document: MockDocument;
  let backend: JsonpClientBackend;

  function runOnlyCallback(home: any, data: Object) {
    const keys = Object.keys(home);
    expect(keys.length).toBe(1);
    const callback = home[keys[0]];
    callback(data);
  }

  beforeEach(() => {
    const mockDoc = new MockDocument();
    TestBed.configureTestingModule({
      providers: [
        JsonpClientBackend,
        {provide: JsonpCallbackContext, useValue: {}},
        {provide: DOCUMENT, useValue: mockDoc},
        {provide: CSP_NONCE, useValue: null},
      ],
    });
    backend = TestBed.inject(JsonpClientBackend);
    home = TestBed.inject(JsonpCallbackContext);
    document = mockDoc;
  });

  it('handles a basic request', (done) => {
    backend
      .handle(SAMPLE_REQ)
      .pipe(toArray())
      .subscribe((events) => {
        expect(events.map((event) => event.type)).toEqual([
          HttpEventType.Sent,
          HttpEventType.Response,
        ]);
        done();
      });
    runOnlyCallback(home, {data: 'This is a test'});
    document.mockLoad();
  });
  // Issue #39496
  it('handles a request with callback call wrapped in promise', (done) => {
    backend.handle(SAMPLE_REQ).subscribe({complete: done});
    queueMicrotask(() => {
      runOnlyCallback(home, {data: 'This is a test'});
    });
    document.mockLoad();
  });
  it('handles an error response properly', (done) => {
    const error = new Error('This is a test error');
    backend
      .handle(SAMPLE_REQ)
      .pipe(toArray())
      .subscribe(undefined, (err: HttpErrorResponse) => {
        expect(err.status).toBe(0);
        expect(err.error).toBe(error);
        done();
      });
    document.mockError(error);
  });
  it('prevents the script from executing when the request is cancelled', () => {
    const sub = backend.handle(SAMPLE_REQ).subscribe();
    expect(Object.keys(home).length).toBe(1);
    const keys = Object.keys(home);
    const spy = jasmine.createSpy('spy', home[keys[0]]);

    sub.unsubscribe();
    document.mockLoad();
    expect(Object.keys(home).length).toBe(0);
    expect(spy).not.toHaveBeenCalled();
    // The script element should have been transferred to a different document to prevent it from
    // executing.
    expect(document.mock!.ownerDocument).not.toEqual(document);
  });
  describe('CSP nonce', () => {
    it('sets nonce attribute on script element when CSP_NONCE token is provided', (done) => {
      TestBed.resetTestingModule();
      const mockDoc = new MockDocument();
      TestBed.configureTestingModule({
        providers: [
          JsonpClientBackend,
          {provide: JsonpCallbackContext, useValue: {}},
          {provide: DOCUMENT, useValue: mockDoc},
          {provide: CSP_NONCE, useValue: 'test-nonce-123'},
        ],
      });
      const nonceBackend = TestBed.inject(JsonpClientBackend);
      nonceBackend.handle(SAMPLE_REQ).subscribe();

      expect(mockDoc.mock!.getAttribute('nonce')).toBe('test-nonce-123');
      done();
    });

    it('does not set nonce attribute when CSP_NONCE token is not provided', (done) => {
      backend.handle(SAMPLE_REQ).subscribe();

      expect(document.mock!.getAttribute('nonce')).toBeNull();
      done();
    });
  });

  describe('throws an error', () => {
    it('when request method is not JSONP', () =>
      expect(() => backend.handle(SAMPLE_REQ.clone<never>({method: 'GET'}))).toThrowError(
        `NG02810: ${JSONP_ERR_WRONG_METHOD}`,
      ));
    it('when response type is not json', () =>
      expect(() =>
        backend.handle(
          SAMPLE_REQ.clone<never>({
            responseType: 'text',
          }),
        ),
      ).toThrowError(`NG02811: ${JSONP_ERR_WRONG_RESPONSE_TYPE}`));
    it('when headers are set in request', () =>
      expect(() =>
        backend.handle(
          SAMPLE_REQ.clone<never>({
            headers: new HttpHeaders({'Content-Type': 'application/json'}),
          }),
        ),
      ).toThrowError(`NG02812: ${JSONP_ERR_HEADERS_NOT_SUPPORTED}`));
    it('when callback is never called', (done) => {
      backend.handle(SAMPLE_REQ).subscribe(undefined, (err: HttpErrorResponse) => {
        expect(err.status).toBe(0);
        expect(err.error instanceof Error).toEqual(true);
        expect(err.error.message).toEqual(JSONP_ERR_NO_CALLBACK);
        done();
      });
      document.mockLoad();
    });
  });
});
