import { IEventAggregationService } from '@studyportals/event-aggregation-service-interface';
import { StudentField } from '@studyportals/studentdomain';
import { suite, test } from '@testdeck/mocha';
import { assert } from 'chai';
import * as Moq from 'typemoq';
import { IMock } from 'typemoq/Api/IMock';
import { StudentRepositoryStateType } from '../../interfaces/enumerations';
import { AnonymousStudentEventBroadcaster } from '../../src/infrastructure/anonymous-student-event-broadcaster';
import { CachedStudentClient } from '../../src/infrastructure/clients/cached-student-client';
import { LocalStudentClient } from '../../src/infrastructure/clients/local-student-client';
import { StudentAPIClient } from '../../src/infrastructure/clients/student-api-client';
import { StudentClient } from '../../src/infrastructure/interfaces/student-client';

@suite
class CachedStudentClientTest {

	protected testInstanceMock: IMock<CachedStudentClient>;

	protected get testInstance(): CachedStudentClient {
		return this.testInstanceMock.object;
	}

	protected studentAPIClientMock: IMock<StudentClient>;

	protected get studentAPIClient(): StudentClient {
		return this.studentAPIClientMock.object;
	}

	protected localStudentClientMock: IMock<StudentClient>;

	protected get localStudentClient(): StudentClient {
		return this.localStudentClientMock.object;
	}

	protected eventAggregationServiceMock: IMock<IEventAggregationService>;

	protected get eventAggregationService(): IEventAggregationService {
		return this.eventAggregationServiceMock.object;
	}

	protected anonymousStudentEventBroadcasterMock: IMock<AnonymousStudentEventBroadcaster>;

	protected get anonymousStudentEventBroadcaster(): AnonymousStudentEventBroadcaster {
		return this.anonymousStudentEventBroadcasterMock.object;
	}

	public before(): void {
		this.studentAPIClientMock = Moq.Mock.ofType(StudentAPIClient);
		this.localStudentClientMock = Moq.Mock.ofType(LocalStudentClient);
		this.eventAggregationServiceMock = Moq.Mock.ofType<IEventAggregationService>();
		this.anonymousStudentEventBroadcasterMock = Moq.Mock.ofType(AnonymousStudentEventBroadcaster);

		const instance = new CachedStudentClient(
			this.studentAPIClient as StudentAPIClient,
			this.localStudentClient as LocalStudentClient,
			this.eventAggregationService,
			this.anonymousStudentEventBroadcaster,
		);

		this.testInstanceMock = Moq.Mock.ofInstance(instance);
		this.testInstanceMock.callBase = true;
	}

	@test
	public async notify_Should_Not_Broadcast_When_Event_Is_Not_StudentProfileChanged(): Promise<void> {
		// given
		const providedEventType = 'SomeRandomEvent';

		// when
		await this.testInstance.notify({eventType: providedEventType} as any);

		// then
		this.anonymousStudentEventBroadcasterMock.verify((x) => x.broadcastProfileUpdatedEvent(Moq.It.isAny()), Moq.Times.never());
	}

	@test
	public async notify_Should_Broadcast_Correct_Changes_When_Called_With_Updates(): Promise<void> {
		// given
		const providedEvent = {
			eventType: 'StudentProfileChanged',
			updated: {
				first_name: 'ABC',
				last_name: 'XYZ',
			},
		};

		const expectedEvent = {
			state: StudentRepositoryStateType.ONLINE,
			changes: providedEvent.updated,
		} as any;

		// when
		await this.testInstance.notify(providedEvent as any);

		// then
		this.anonymousStudentEventBroadcasterMock.verify((x) => x.broadcastProfileUpdatedEvent(Moq.It.isObjectWith(expectedEvent)), Moq.Times.once());
	}

	@test
	public async notify_Should_Broadcast_Correct_Deletion_When_Called_With_Deletes(): Promise<void> {
		// given
		const providedEvent = {
			eventType: 'StudentProfileChanged',
			deleted: {
				first_name: null,
				last_name: null,
			},
		};

		const expectedEvent = {
			state: StudentRepositoryStateType.ONLINE,
			isLocal: false,
			changes: {
				first_name: undefined,
				last_name: undefined,
			},
		} as any;

		// when
		await this.testInstance.notify(providedEvent as any);

		// then
		this.anonymousStudentEventBroadcasterMock.verify((x) => x.broadcastProfileUpdatedEvent(Moq.It.isObjectWith(expectedEvent)), Moq.Times.once());
	}

	@test
	public async notify_Should_BroadcastIsLocalTrue_When_EventHashIsLocalHash(): Promise<void> {
		// given
		const providedEvent = {
			eventType: 'StudentProfileChanged',
			updated: {
				[StudentField.FIRST_NAME]: 'ABC',
				[StudentField.LAST_STATE_CHANGE_HASH]: 'someHash',
			},
		};

		const expectedEvent = {
			state: StudentRepositoryStateType.ONLINE,
			isLocal: true,
			changes: {
				[StudentField.FIRST_NAME]: 'ABC',
				[StudentField.LAST_STATE_CHANGE_HASH]: 'someHash',
			},
		} as any;

		this.testInstance['localStateHashes'] = ['someHash'];

		// when
		await this.testInstance.notify(providedEvent as any);

		// then
		this.anonymousStudentEventBroadcasterMock.verify((x) => x.broadcastProfileUpdatedEvent(Moq.It.isObjectWith(expectedEvent)), Moq.Times.once());
	}

	@test
	public async notify_Should_Save_Changes_In_LocalStorage_When_Correct_Event(): Promise<void> {
		// given
		const providedEvent = {
			eventType: 'StudentProfileChanged',
			updated: {
				last_login: 434,
			},
			deleted: {
				first_name: null,
				last_name: null,
			},
		};

		const expectedData = {
			last_login: 434,
			first_name: undefined,
			last_name: undefined,
		};

		// when
		await this.testInstance.notify(providedEvent as any);

		// then
		this.localStudentClientMock.verify((x) => x.setData(Moq.It.isObjectWith(expectedData)), Moq.Times.once());
	}

	@test
	public async addToCollection_Should_AddToCollection_Remote_AND_Local_When_Called_With_Disciplines(): Promise<void> {
		// given
		const expectedIds = [1, 2, 3];

		// when
		await this.testInstance.addToCollection(StudentField.DISCIPLINES, expectedIds);

		// then
		this.studentAPIClientMock.verify((x) => x.addToCollection(StudentField.DISCIPLINES, expectedIds), Moq.Times.once());
		this.localStudentClientMock.verify((x) => x.addToCollection(StudentField.DISCIPLINES, expectedIds), Moq.Times.once());
	}

	@test
	public async addToCollection_Should_AddLocalHash_When_Called_With_Disciplines(): Promise<void> {
		// given
		const expectedLastStateChangeHash = 'someSecretHash';
		const expectedIds = [1, 2, 3];

		this.studentAPIClientMock
			.setup((x) => x.addToCollection(StudentField.DISCIPLINES, expectedIds))
			.returns(async () => expectedLastStateChangeHash);

		// when
		await this.testInstance.addToCollection(StudentField.DISCIPLINES, expectedIds);

		// then
		assert.equal(this.testInstance['localStateHashes'][0], expectedLastStateChangeHash);
	}

	@test
	public async removeFromCollection_Should_RemoveFromCollection_Remote_AND_Local_When_Called_With_Disciplines(): Promise<void> {
		// given
		const expectedIds = [1, 2, 3];

		// when
		await this.testInstance.removeFromCollection(StudentField.DISCIPLINES, expectedIds);

		// then
		this.studentAPIClientMock.verify((x) => x.removeFromCollection(StudentField.DISCIPLINES, expectedIds), Moq.Times.once());
		this.localStudentClientMock.verify((x) => x.removeFromCollection(StudentField.DISCIPLINES, expectedIds), Moq.Times.once());
	}

	@test
	public async removeFromCollection_Should_AddLocalHash_When_Called_With_Disciplines(): Promise<void> {
		// given
		const expectedLastStateChangeHash = 'someSecretHash';
		const expectedIds = [1, 2, 3];

		this.studentAPIClientMock
			.setup((x) => x.removeFromCollection(StudentField.DISCIPLINES, expectedIds))
			.returns(async () => expectedLastStateChangeHash);

		// when
		await this.testInstance.removeFromCollection(StudentField.DISCIPLINES, expectedIds);

		// then
		assert.equal(this.testInstance['localStateHashes'][0], expectedLastStateChangeHash);
	}

	@test
	public async setData_Should_SetData_Remote_AND_Local_WHen_Called_With_Data(): Promise<void> {
		// given
		const expectedData = {
			first_name: 'SomeName',
		};

		// when
		await this.testInstance.setData(expectedData);

		// then
		this.studentAPIClientMock.verify((x) => x.setData(expectedData), Moq.Times.once());
		this.localStudentClientMock.verify((x) => x.setData(expectedData), Moq.Times.once());
	}

	@test
	public async setData_Should_AddLocalHash_When_Called_With_Data(): Promise<void> {
		// given
		const expectedData = {
			first_name: 'SomeName',
		};
		const expectedHash = 'someHash';

		this.studentAPIClientMock
			.setup((x) => x.setData(expectedData))
			.returns(async () => expectedHash);

		// when
		await this.testInstance.setData(expectedData);

		// then
		assert.equal(this.testInstance['localStateHashes'][0], expectedHash);
	}

	@test
	public async getData_Should_Return_Data_From_LocalClient_When_All_Fields_Are_Cached(): Promise<void> {
		// given
		const providedStudentFields = [StudentField.FIRST_NAME, StudentField.LAST_NAME];
		const providedLocalData = {[StudentField.FIRST_NAME]: 'myName', [StudentField.LAST_NAME]: undefined};

		this.localStudentClientMock
			.setup((x) => x.getData(Moq.It.isAny()))
			.returns(async () => providedLocalData);

		// when
		const result = await this.testInstance.getData(providedStudentFields);

		// then
		assert.equal(result, providedLocalData);
	}

	@test
	public async getData_Should_Only_Request_Fields_When_Fields_Are_Not_Cached(): Promise<void> {
		// given
		const providedStudentFields = [StudentField.FIRST_NAME, StudentField.LAST_NAME];
		const providedLocalData = {[StudentField.LAST_NAME]: undefined};

		this.localStudentClientMock
			.setup((x) => x.getData(Moq.It.isAny()))
			.returns(async () => providedLocalData);

		// when
		const result = await this.testInstance.getData(providedStudentFields);

		// then
		this.studentAPIClientMock.verify((x) => x.getData([StudentField.FIRST_NAME]), Moq.Times.once());
	}

	@test
	public async getData_Should_Merge_Data_From_Local_And_Remote_When_Fields_Are_Both_Cached_And_Not_Cached(): Promise<void> {
		// given
		const providedStudentFields = [StudentField.FIRST_NAME, StudentField.LAST_NAME];
		const providedLocalData = {[StudentField.LAST_NAME]: undefined};
		const providedRemoteData = {[StudentField.FIRST_NAME]: 'MyFirstName'};

		this.localStudentClientMock
			.setup((x) => x.getData(Moq.It.isAny()))
			.returns(async () => providedLocalData);

		this.studentAPIClientMock
			.setup((x) => x.getData(Moq.It.isAny()))
			.returns(async () => providedRemoteData);

		const expectedData = {...providedLocalData, ...providedRemoteData};

		// when
		const result = await this.testInstance.getData(providedStudentFields);

		// then
		assert.deepEqual(result, expectedData);
	}

}
