import { HttpService } from "@nestjs/axios";
import { CacheModule, CACHE_MANAGER } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
 import { Test } from "@nestjs/testing";
import { Observable, of } from "rxjs";
import { EsTime, Order } from "src/core/domain";
import { PageDto } from "src/core/domain/dtos";
import { HttpResponseException } from "src/core/exceptions";
import { PageInterceptor } from "src/core/interceptors/page.interceptor";

const execCtxMock = {
    switchToHttp:       jest.fn().mockReturnThis(),
    getRequest:         jest.fn().mockReturnThis(),
    getHandler:         jest.fn().mockReturnThis(),
    getArgs:            jest.fn().mockReturnThis(),
    getArgByIndex:      jest.fn().mockReturnThis(),
    switchToRpc:        jest.fn().mockReturnThis(),
    switchToWs:         jest.fn().mockReturnThis(),
    getType:            jest.fn().mockReturnThis(),
    getClass:           jest.fn().mockReturnThis(),
  };

const callHandlerMock = {
    handle: jest.fn(),
};

describe('Unit tests for PageInterceptor', () => {
    let pageInter: PageInterceptor;
    let httpService: HttpService;

    beforeAll(async () => {
         const moduleRef = await Test.createTestingModule({
            providers: [
                {
                    provide: HttpService,
                    useValue: {
                        post: jest.fn(),
                        delete: jest.fn()
                    },
                },
                PageInterceptor,
            ],
            imports: [
                ConfigModule.forRoot({
                    isGlobal: true,
                    cache: true,
                    expandVariables: true,
                }),
                CacheModule.register()
            ],
        }).compile();

        pageInter = moduleRef.get(PageInterceptor);
        httpService = moduleRef.get(HttpService);

        execCtxMock.getRequest.mockReturnValue({
            query: {
                query: 'thisIsMyQuery',
                page: 1,
                limit: 5,
                order: Order.DESC
            }
        });

        callHandlerMock.handle.mockReturnValue(
            of({
                hits: {
                    total: { value: 1 },
                    hits: [{}]
                }
            })
        );
    });

    it('Should be defined', () => {
        expect(pageInter).toBeDefined();
        expect(httpService).toBeDefined();
    });

    describe('intercept()', () => {
        let tmp;
        beforeAll(() => {
            tmp =  pageInter.getPIT;
            pageInter.getPIT = jest.fn().mockReturnValue({});
        });

        afterAll(() => {
            pageInter.getPIT = tmp;
        });

        it('Should return a Promise', () => {
            expect(pageInter.intercept(execCtxMock, callHandlerMock)).toBeInstanceOf(Promise);
        });

        it('Should return a Promise with Observable and PageDto inside', () => {
            pageInter.intercept(execCtxMock, callHandlerMock).then((res) => {
                expect(res).toBeInstanceOf(Observable);
                res.subscribe((data) => {
                    expect(data).toBeInstanceOf(PageDto);
                });
            });
        });

        it('Should touch CallHandler.handle() method', () => {
            let chHandleSpy = jest.spyOn(callHandlerMock, 'handle');
            pageInter.intercept(execCtxMock, callHandlerMock);
            expect(chHandleSpy).toBeCalled();
        });

        it('Should construct a page with proper data on it', () => {
            callHandlerMock.handle.mockReturnValueOnce(
                of({
                    hits: {
                        total: { value: 1 },
                        hits: [{
                            _source: {
                                dummy: 'dum'
                            }
                        }]
                    }
                })
            );

            pageInter.intercept(execCtxMock, callHandlerMock).then((res) => {
                res.subscribe((page) => {
                    expect(page.data.length).toBe(1);
                    expect(page.data[0]).toEqual({ dummy: 'dum' });
                });
            });
        });

        it('Should construct correct meta-data of the page', () => {
            execCtxMock.getRequest.mockReturnValueOnce({
                query: {
                    page: 5,
                    order: 'desc',
                    limit: 100,
                }
            });

            callHandlerMock.handle.mockReturnValueOnce(
                of({
                    hits: {
                        total: { value: 921 },
                        hits: []
                    }
                })
            );

            pageInter.intercept(execCtxMock, callHandlerMock).then((res) => {
                res.subscribe((page) => {
                    expect(page.meta).toEqual({
                        total: 921,
                        order: 'desc',
                    });
                });
            });
        });     
    });

    describe('getPIT()', () => {
        it('Should touch HttpService.post() method', () => {
            let httpPostMock = jest.spyOn(httpService, 'post').mockReturnValueOnce(of({
                data: {id: '2567'},
                status: 0,
                statusText: '',
                headers: {},
                config: {},
            }));

            pageInter.getPIT(1);
            expect(httpPostMock).toHaveBeenCalled();
        });

        it('Should contain correct port in the URI from .env', () => {
            let httpPostMock = jest.spyOn(httpService, 'post').mockReturnValueOnce(of({
                data: {id: '2567'},
                status: 0,
                statusText: '',
                headers: {},
                config: {},
            }));

            pageInter.getPIT(1);
            expect(httpPostMock).toHaveBeenCalledWith(`http://${process.env.ES_CONTAINER_NAME}:${process.env.ES_PORT}/papers/_pit?keep_alive=1m`);
        });

        it('Should touch HttpService with correct URI when time alive and time-unit are set', () => {
            let httpPostMock = jest.spyOn(httpService, 'post').mockReturnValueOnce(of({
                data: {id: '2567'},
                status: 0,
                statusText: '',
                headers: {},
                config: {},
            }));

            let time = 2;
            let unit = EsTime.sec;
            
            pageInter.getPIT(time, unit);
            expect(httpPostMock).toHaveBeenCalledWith(`http://${process.env.ES_CONTAINER_NAME}:${process.env.ES_PORT}/papers/_pit?keep_alive=${time+unit}`);
        });

        it('Should return error exeception when HttpService fails', () => {
            jest.spyOn(httpService, 'post').mockImplementationOnce(() => {
                throw HttpResponseException;
            });

            expect(pageInter.getPIT(1)).rejects.toEqual(HttpResponseException);
        });

        it('Should return a non-empty string when HttpService request succeedes', () => {
            jest.spyOn(httpService, 'post').mockReturnValueOnce(of({
                data: {id: '2567', keep_alive: '1m'},
                status: 0,
                statusText: '',
                headers: {},
                config: {},
            }));

            expect(pageInter.getPIT(1)).resolves.toEqual({
                id: '2567',
                keep_alive: '1m',
            });
        });
    });

    describe('deletePIT()', () => {
        it('Should touch HttpService.delete() method', () => {
            let httpDeleteMock = jest.spyOn(httpService, 'delete').mockReturnValueOnce(
                of({
                    data: {succeeded: true},
                    status: 0,
                    statusText: '',
                    headers: {},
                    config: {},
                }));

            pageInter.deletePIT('');
            expect(httpDeleteMock).toHaveBeenCalled();
        });

        it('Should contain correct port in the URI from .env and passed PIT ID in the request body', () => {
            let httpDeleteMock = jest.spyOn(httpService, 'delete').mockReturnValueOnce(
                of({
                    data: { succeeded: true },
                    status: 0,
                    statusText: '',
                    headers: {},
                    config: {},
                }));

            pageInter.deletePIT('thisIsIDSpecified');
            expect(httpDeleteMock).toHaveBeenCalledWith(`http://${process.env.ES_CONTAINER_NAME}:${process.env.ES_PORT}/_pit`, {
                data: { id: 'thisIsIDSpecified' },
                headers: { 'Content-Type': 'application/json' }
            });
        });

        it('Should return error exeception when HttpService fails', () => {
            jest.spyOn(httpService, 'delete').mockImplementationOnce(() => {
                throw HttpResponseException;
            });

            expect(pageInter.deletePIT('')).rejects.toEqual(HttpResponseException);
        });

        it('Should return true when Elasticsearch successfully removed PIT', () => {
            jest.spyOn(httpService, 'delete').mockReturnValueOnce(
                of({
                    data: { succeeded: true },
                    status: 0,
                    statusText: '',
                    headers: {},
                    config: {},
                }));

            expect(pageInter.deletePIT('')).resolves.toBe(true);
        });
    });

    afterEach(() => {
        pageInter['cacheManager'].reset();
    })
});