import { DOCUMENT } from '@angular/common';
import { HTTP_INTERCEPTORS, HttpClient, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController, TestRequest } from '@angular/common/http/testing';
import { Type } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { BixiAuthModule } from '../auth.module';
import { BIXI_AUTH_CONFIG, BixiAuthConfig } from '../config.type';
import { AuthReferrer, BIXI_SERVICE_TOKEN, ITokenModel, ITokenService } from './interface';
import { SimpleInterceptor } from './simple/simple.interceptor';
import { SimpleTokenModel } from './simple/simple.model';

function genModel<T extends ITokenModel>(modelType: new () => T, token: string | null = `123`) {
  const model: any = new modelType();
  model.token = token;
  model.uid = 1;
  return model;
}

class MockTokenService implements ITokenService {
  // tslint:disable-next-line:no-any
  [key: string]: any;

  // tslint:disable-next-line:no-any
  _data: any;
  // tslint:disable-next-line:no-any
  options: any;
  referrer: AuthReferrer = {};

  set(data: ITokenModel): boolean {
    this._data = data;
    return true;
  }

  get(): ITokenModel {
    return this._data;
  }

  // tslint:disable-next-line:no-any
  change(): any {
    return null;
  }

  clear() {
    this._data = null;
  }

  get login_url() {
    return '/login';
  }
}

let otherRes = new HttpResponse();

class OtherInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req.clone()).pipe(
      catchError(() => {
        return throwError(otherRes);
      })
    );
  }
}

describe('auth: base.interceptor', () => {
  let http: HttpClient;
  let httpBed: HttpTestingController;
  let router: Router;
  const MockDoc = {
    location: {
      href: ''
    }
  };

  // tslint:disable-next-line:no-any
  function genModule(options: BixiAuthConfig, tokenData?: ITokenModel, provider: any[] = []) {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule, RouterTestingModule.withRoutes([]), BixiAuthModule],
      providers: [
        { provide: DOCUMENT, useValue: MockDoc },
        { provide: BIXI_AUTH_CONFIG, useValue: { ...options } },
        {
          provide: HTTP_INTERCEPTORS,
          useClass: SimpleInterceptor,
          multi: true
        },
        { provide: BIXI_SERVICE_TOKEN, useClass: MockTokenService }
      ].concat(provider)
    });
    if (tokenData) TestBed.inject(BIXI_SERVICE_TOKEN).set(tokenData);

    router = TestBed.inject<Router>(Router);
    spyOn(router, 'navigate');
    http = TestBed.inject<HttpClient>(HttpClient);
    httpBed = TestBed.inject(HttpTestingController as Type<HttpTestingController>);
  }

  describe('[ignores]', () => {
    describe('#with config', () => {
      const basicModel = genModel(SimpleTokenModel);

      it(`should be ignore /login`, (done: () => void) => {
        genModule({ ignores: [/assets\//, /\/login/] }, basicModel);

        http.get('/login', { responseType: 'text' }).subscribe(done);
        const req = httpBed.expectOne('/login') as TestRequest;
        expect(req.request.headers.get('token')).toBeNull();
        req.flush('ok!');
      });

      it('should be non-ignore', (done: () => void) => {
        genModule({ ignores: [] }, basicModel);
        http.get('/login', { responseType: 'text' }).subscribe(done);
        const req = httpBed.expectOne('/login') as TestRequest;
        expect(req.request.headers.get('token')).toBe('123');
        req.flush('ok!');
      });
    });

    describe('#with allow_anonymous_key', () => {
      it(`in params`, (done: () => void) => {
        genModule({}, genModel(SimpleTokenModel, null));
        http.get('/user', { responseType: 'text', params: { _allow_anonymous: '' } }).subscribe(done);
        const ret = httpBed.expectOne(() => true);
        expect(ret.request.headers.get('Authorization')).toBeNull();
        ret.flush('ok!');
      });
      it(`in params (full url)`, (done: () => void) => {
        genModule({}, genModel(SimpleTokenModel, null));
        http
          .get('https://bixi.datagrand.com/api/user', {
            responseType: 'text',
            params: { _allow_anonymous: '' }
          })
          .subscribe(done);
        const ret = httpBed.expectOne(() => true);
        expect(ret.request.headers.get('Authorization')).toBeNull();
        ret.flush('ok!');
      });
      it(`in url`, (done: () => void) => {
        genModule({}, genModel(SimpleTokenModel, null));
        http.get('/user?_allow_anonymous=1', { responseType: 'text' }).subscribe(done);
        const ret = httpBed.expectOne(() => true);
        expect(ret.request.headers.get('Authorization')).toBeNull();
        ret.flush('ok!');
      });
      it(`in url (full url)`, (done: () => void) => {
        genModule({}, genModel(SimpleTokenModel, null));
        http.get('https://bixi.datagrand.com/api/user?_allow_anonymous=1', { responseType: 'text' }).subscribe(done);
        const ret = httpBed.expectOne(() => true);
        expect(ret.request.headers.get('Authorization')).toBeNull();
        ret.flush('ok!');
      });
    });
  });

  describe('[invalid token]', () => {
    describe('should be navigate to login', () => {
      it('with navigate', (done: () => void) => {
        genModule({}, genModel(SimpleTokenModel, null));
        http.get('/test', { responseType: 'text' }).subscribe(
          () => {
            expect(false).toBe(true);
            done();
          },
          // tslint:disable-next-line:no-any
          (err: any) => {
            expect(err.status).toBe(401);
            setTimeout(() => {
              expect(TestBed.inject<Router>(Router).navigate).toHaveBeenCalled();
              done();
            }, 20);
          }
        );
      });
      it('with location', (done: () => void) => {
        const login_url = 'https://bixi.datagrand.com/login';
        genModule({ login_url }, genModel(SimpleTokenModel, null));
        http.get('/test', { responseType: 'text' }).subscribe(
          () => {
            expect(false).toBe(true);
            done();
          },
          // tslint:disable-next-line:no-any
          (err: any) => {
            expect(err.status).toBe(401);
            setTimeout(() => {
              expect(TestBed.inject(DOCUMENT).location.href).toBe(login_url);
              done();
            }, 20);
          }
        );
      });
    });

    it('should be not navigate to login when token_invalid_redirect: false', (done: () => void) => {
      genModule({ token_invalid_redirect: false }, genModel(SimpleTokenModel, null));
      http.get('/test', { responseType: 'text' }).subscribe(
        () => {
          expect(false).toBe(true);
          done();
        },
        // tslint:disable-next-line:no-any
        (err: any) => {
          expect(err.status).toBe(401);
          done();
        }
      );
    });
  });

  describe('[referrer]', () => {
    it('should be always router url', (done: () => void) => {
      genModule({ executeOtherInterceptors: false }, genModel(SimpleTokenModel, null));
      http.get('/to-test', { responseType: 'text' }).subscribe(
        () => {
          expect(false).toBe(true);
          done();
        },
        () => {
          const tokenSrv = TestBed.inject(BIXI_SERVICE_TOKEN) as MockTokenService;
          expect(tokenSrv.referrer).not.toBeNull();
          expect(tokenSrv.referrer.url).toBe('/');
          done();
        }
      );
    });
  });

  describe('[executeOtherInterceptors]', () => {
    beforeEach(() => {
      genModule({ executeOtherInterceptors: true }, genModel(SimpleTokenModel, null), [
        { provide: HTTP_INTERCEPTORS, useClass: OtherInterceptor, multi: true }
      ]);
    });

    it('shoul working', done => {
      otherRes = new HttpResponse({ body: { a: 1 } });
      const url = '/to-test';
      http.get(url, { responseType: 'text' }).subscribe(
        () => {
          expect(false).toBe(true);
          done();
        },
        err => {
          expect(err.body.a).toBe(1);
          done();
        }
      );
    });
  });
});
