import { IEventAggregationService } from '@studyportals/event-aggregation-service-interface';
import { ITokenBasedSessionService } from '@studyportals/student-interfaces';
import { AttendanceType, IStudent, StudentField } from '@studyportals/studentdomain';
import { suite, test } from '@testdeck/mocha';
import { assert } from 'chai';
import * as Moq from 'typemoq';
import { Actor, StudentRepositoryStateType } from '../../interfaces';
import { OfflineStudentRepositoryState } from '../../src/domain/states/offline-student-repository-state';
import { OnlineStudentRepositoryState } from '../../src/domain/states/online-student-repository-state';
import { PendingStudentRepositoryState } from '../../src/domain/states/pending-student-repository-state';
import { StudentRepository } from '../../src/domain/student-repository';
import { AnonymousStudentEventBroadcaster } from '../../src/infrastructure/anonymous-student-event-broadcaster';

@suite()
class StudentRepositoryTest {
	private testInstanceMock: Moq.IMock<StudentRepository>;

	private pendingStudentRepositoryStateMock: Moq.IMock<PendingStudentRepositoryState>;
	private onlineStudentRepositoryStateMock: Moq.IMock<OnlineStudentRepositoryState>;
	private offlineStudentRepositoryStateMock: Moq.IMock<OfflineStudentRepositoryState>;
	private anonymousStudentEventBroadcasterMock: Moq.IMock<AnonymousStudentEventBroadcaster>;
	private eventAggregationServiceMock: Moq.IMock<IEventAggregationService>;
	private sessionServiceMock: Moq.IMock<ITokenBasedSessionService>;

	private get sessionService(): ITokenBasedSessionService {
		return this.sessionServiceMock.object;
	}

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

	private get pendingStudentRepositoryState(): PendingStudentRepositoryState {
		return this.pendingStudentRepositoryStateMock.object;
	}

	private get onlineStudentRepositoryState(): OnlineStudentRepositoryState {
		return this.onlineStudentRepositoryStateMock.object;
	}

	private get offlineStudentRepositoryState(): OfflineStudentRepositoryState {
		return this.offlineStudentRepositoryStateMock.object;
	}

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

	private get testInstance(): StudentRepository {
		return this.testInstanceMock.object;
	}

	public before(): void {
		this.sessionServiceMock = Moq.Mock.ofType<ITokenBasedSessionService>();
		this.eventAggregationServiceMock = Moq.Mock.ofType<IEventAggregationService>();
		this.pendingStudentRepositoryStateMock = Moq.Mock.ofType<PendingStudentRepositoryState>();
		this.onlineStudentRepositoryStateMock = Moq.Mock.ofType<OnlineStudentRepositoryState>();
		this.offlineStudentRepositoryStateMock = Moq.Mock.ofType<OfflineStudentRepositoryState>();
		this.anonymousStudentEventBroadcasterMock = Moq.Mock.ofType<AnonymousStudentEventBroadcaster>();

		this.testInstanceMock = Moq.Mock.ofType2(StudentRepository, [this.eventAggregationService, this.sessionService]);
		this.testInstanceMock.callBase = true;

		this.testInstance['state'] = this.pendingStudentRepositoryState;
		this.testInstance['onlineState' as any] = this.onlineStudentRepositoryState;
		this.testInstance['pendingState' as any] = this.pendingStudentRepositoryState;
		this.testInstance['offlineState' as any] = this.offlineStudentRepositoryState;

		this.offlineStudentRepositoryStateMock
			.setup((x) => x['stateType'])
			.returns(() => StudentRepositoryStateType.OFFLINE);

		this.onlineStudentRepositoryStateMock
			.setup((x) => x['stateType'])
			.returns(() => StudentRepositoryStateType.ONLINE);

		this.pendingStudentRepositoryStateMock
			.setup((x) => x['stateType'])
			.returns(() => StudentRepositoryStateType.PENDING);

		this.testInstanceMock
			.setup((x) => x['anonymousStudentEventBroadcaster'])
			.returns(() => this.anonymousStudentEventBroadcaster);
	}

	@test
	public async initialize_Should_CallInitializeOnAllStates_When_Called(): Promise<void> {
		// given

		// when
		this.testInstance.initialize();

		// then
		this.pendingStudentRepositoryStateMock
			.verify(
				(x) => x.initialize(),
				Moq.Times.once()
			);
		this.offlineStudentRepositoryStateMock
			.verify(
				(x) => x.initialize(),
				Moq.Times.once()
			);
		this.onlineStudentRepositoryStateMock
			.verify(
				(x) => x.initialize(),
				Moq.Times.once()
			);

	}

	@test
	public async setStudentData_Should_Call_Current_State_setStudentData_When_Called(): Promise<void> {
		// given
		const expectedData: IStudent = {[StudentField.GENDER]: 'm'};
		const expectedActor = Actor.USER;

		this.pendingStudentRepositoryStateMock.setup((x) => x.setStudentData(Moq.It.isAny(), Moq.It.isAny()));

		// when
		await this.testInstance.setStudentData(expectedData, expectedActor);

		// then
		this.pendingStudentRepositoryStateMock.verify((x) => x.setStudentData(expectedData, expectedActor), Moq.Times.once());
	}

	@test
	public async getStudentData_Should_Call_Current_State_getStudentData_And_Construct_The_Return_Object_When_Called(): Promise<void> {
		// given
		const listOfStudentFields = [StudentField.BIRTH_DATE, StudentField.EMAIL, StudentField.NAME];
		const providedStudentData = {
			[StudentField.BIRTH_DATE]: 'jack shit',
			[StudentField.EMAIL]: 'purple',
		};
		this.pendingStudentRepositoryStateMock
			.setup((x) => x.getStudentData(Moq.It.isAny()))
			.returns(async () => providedStudentData);

		const expectedStudentData = {
			...providedStudentData,
			...{[StudentField.NAME]: undefined},
		};

		// when
		const actualStudentData = await this.testInstance.getStudentData(listOfStudentFields);

		// then
		this.pendingStudentRepositoryStateMock.verify((x) => x.getStudentData(listOfStudentFields), Moq.Times.once());
		assert.deepEqual(actualStudentData, expectedStudentData);
	}

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

		const providedStudentFields = [StudentField.FIRST_NAME, StudentField.LAST_NAME, StudentField.GENDER];

		this.testInstanceMock
			.setup((x) => x.getStudentData(Moq.It.isAny()))
			.returns(async () => providedData);

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

		// then
		assert.equal(result, 2 / 3);
	}

	@test
	public async getStudentDataCompleteness_Should_Completeness_1_When_When_3_Out_Of_3_Fields_Are_Set(): Promise<void> {
		// given
		const providedData = {
			[StudentField.FIRST_NAME]: 'myName',
			[StudentField.LAST_NAME]: 'someName',
			[StudentField.GENDER]: 'f',
		} as any;

		const providedStudentFields = [StudentField.FIRST_NAME, StudentField.LAST_NAME, StudentField.GENDER];

		this.testInstanceMock
			.setup((x) => x.getStudentData(Moq.It.isAny()))
			.returns(async () => providedData);

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

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

	@test
	public async getStudentDataCompleteness_Should_Completeness_0_When_When_0_Out_Of_3_Fields_Are_Set(): Promise<void> {
		// given
		const providedData = {
			[StudentField.FIRST_NAME]: undefined,
			[StudentField.LAST_NAME]: undefined,
			[StudentField.GENDER]: undefined,
		} as any;

		const providedStudentFields = [StudentField.FIRST_NAME, StudentField.LAST_NAME, StudentField.GENDER];

		this.testInstanceMock
			.setup((x) => x.getStudentData(Moq.It.isAny()))
			.returns(async () => providedData);

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

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

	@test
	public async addToCollection_Should_Call_StudentRepository_addToCollection_When_Called(): Promise<void> {
		// given
		this.pendingStudentRepositoryStateMock.setup((x) => x.addToCollection(Moq.It.isAny(), Moq.It.isAny()));

		// when
		await this.testInstance.addToCollection(StudentField.ATTENDANCE, [AttendanceType.ONLINE]);

		// then
		this.pendingStudentRepositoryStateMock.verify((x) => x.addToCollection(Moq.It.isAny(), Moq.It.isAny()), Moq.Times.once());
	}

	@test
	public async removeFromCollection_Should_Call_StudentRepository_addToCollection_When_Called(): Promise<void> {
		// given
		this.pendingStudentRepositoryStateMock.setup((x) => x.removeFromCollection(Moq.It.isAny(), Moq.It.isAny()));

		// when
		await this.testInstance.removeFromCollection(StudentField.INTERESTS_COUNTRIES, [1, 4, 76]);

		// then
		this.pendingStudentRepositoryStateMock.verify((x) => x.removeFromCollection(Moq.It.isAny(), Moq.It.isAny()), Moq.Times.once());
	}

	@test
	public async setGPA_Should_Transfrom_grade_type_And_grade_value_Into__A_GPA_Object_And_Pass_It_To_setStudentData_When_Called(): Promise<void> {
		// given
		const gradeType = 'us_numeric';
		const grade = '4.0';
		const expectedData = {
			gpa: {
				[gradeType]: grade,
				current_type: gradeType,
			},
		};
		const expectedActor = Actor.USER;

		this.testInstanceMock
			.setup((x) => x.setStudentData(Moq.It.isAny(), Moq.It.isAny()));

		// when
		await this.testInstance.setGPA(gradeType, grade);

		// then
		this.testInstanceMock.verify((x) => x.setStudentData(expectedData, expectedActor), Moq.Times.once());
	}

	@test
	public async setName_Should_Transfrom_Name_Into_First_Name_And_Last_name_And_Pass_It_To_setStudentData_When_First_Name_And_Last_Name_Is_Provided(): Promise<void> {
		// given
		const name = 'Jack Black';
		const expectedData = {
			name: 'Jack Black',
			first_name: 'Jack',
			last_name: 'Black',
		};
		const expectedActor = Actor.USER;

		this.testInstanceMock
			.setup((x) => x.setStudentData(Moq.It.isAny(), Moq.It.isAny()));

		// when
		await this.testInstance.setName(name);

		// then
		this.testInstanceMock.verify((x) => x.setStudentData(expectedData, expectedActor), Moq.Times.once());
	}

	@test
	public async setName_Should_Transfrom_Name_Into_First_Name_And_Pass_It_To_setStudentData_When_Only_First_Name_Is_Provided(): Promise<void> {
		// given
		const name = 'Jack';
		const expectedData = {
			name: 'Jack',
			first_name: 'Jack',
			last_name: undefined,
		};
		const expectedActor = Actor.USER;

		this.testInstanceMock
			.setup((x) => x.setStudentData(Moq.It.isAny(), Moq.It.isAny()));

		// when
		await this.testInstance.setName(name);

		// then
		this.testInstanceMock.verify((x) => x.setStudentData(expectedData, expectedActor), Moq.Times.once());
	}

	@test
	public async updateState_Should_SetNewState_When_Called(): Promise<void> {
		// given
		const existingState = this.offlineStudentRepositoryState;
		const newState = this.onlineStudentRepositoryState;

		this.testInstance['state'] = existingState;

		// when
		this.testInstance.updateState(newState);

		// then
		assert.equal(this.testInstance['state'], newState);
	}

	@test
	public async updateState_Should_BroadcastStateChange_When_Called(): Promise<void> {
		// given
		const existingState = this.offlineStudentRepositoryState;
		const newState = this.onlineStudentRepositoryState;

		this.testInstance['state'] = existingState;

		// when
		this.testInstance.updateState(newState);

		// then
		this.anonymousStudentEventBroadcasterMock
			.verify(
				(x) => x.broadcastStateChangedEvent(Moq.It.isObjectWith({
					oldState: StudentRepositoryStateType.OFFLINE,
					newState: StudentRepositoryStateType.ONLINE
				} as any)),
				Moq.Times.once()
			);
	}

}
