/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the Source EULA. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

import * as TypeMoq from 'typemoq';
import { ConnectionConfig } from 'sql/platform/connection/common/connectionConfig';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { WorkspaceConfigurationTestService } from 'sqltest/stubs/workspaceConfigurationTestService';
import * as Constants from 'sql/platform/connection/common/constants';
import { StorageTestService } from 'sqltest/stubs/storageTestService';
import { ConnectionStore } from 'sql/platform/connection/common/connectionStore';
import { CredentialsService } from 'sql/platform/credentials/common/credentialsService';
import * as assert from 'assert';
import { Memento } from 'vs/workbench/common/memento';
import * as azdata from 'azdata';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { ConnectionProfileGroup, IConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
import { ConnectionOptionSpecialType, ServiceOptionType } from 'sql/workbench/api/common/sqlExtHostTypes';
import { CapabilitiesTestService } from '../../stubs/capabilitiesTestService';
import { ConnectionProviderProperties } from 'sql/workbench/parts/connection/common/connectionProviderExtension';

suite('SQL ConnectionStore tests', () => {
	let defaultNamedProfile: IConnectionProfile;
	let context: TypeMoq.Mock<Memento>;
	let credentialStore: TypeMoq.Mock<CredentialsService>;
	let connectionConfig: TypeMoq.Mock<ConnectionConfig>;
	let workspaceConfigurationServiceMock: TypeMoq.Mock<WorkspaceConfigurationTestService>;
	let storageServiceMock: TypeMoq.Mock<StorageTestService>;
	let capabilitiesService: CapabilitiesTestService;
	let mementoArray: any = [];
	let maxRecent = 5;
	let msSQLCapabilities: ConnectionProviderProperties;
	let provider2Capabilities: ConnectionProviderProperties;
	let defaultNamedConnectionProfile: ConnectionProfile;

	setup(() => {
		defaultNamedProfile = Object.assign({}, {
			connectionName: 'new name',
			serverName: 'namedServer',
			databaseName: 'bcd',
			authenticationType: 'SqlLogin',
			userName: 'cde',
			password: 'asdf!@#$',
			savePassword: true,
			groupId: '',
			groupFullName: '',
			getOptionsKey: undefined,
			matches: undefined,
			providerName: 'MSSQL',
			options: {},
			saveProfile: true,
			id: undefined
		});

		storageServiceMock = TypeMoq.Mock.ofType(StorageTestService);

		let momento = new Memento('ConnectionManagement', storageServiceMock.object);
		context = TypeMoq.Mock.ofInstance(momento);
		context.setup(x => x.getMemento(TypeMoq.It.isAny())).returns(() => mementoArray);

		credentialStore = TypeMoq.Mock.ofType(CredentialsService);
		connectionConfig = TypeMoq.Mock.ofType(ConnectionConfig);

		// setup configuration to return maxRecent for the #MRU items

		let configResult: { [key: string]: any } = {};
		configResult[Constants.configMaxRecentConnections] = maxRecent;

		workspaceConfigurationServiceMock = TypeMoq.Mock.ofType(WorkspaceConfigurationTestService);
		workspaceConfigurationServiceMock.setup(x => x.getValue(Constants.sqlConfigSectionName))
			.returns(() => configResult);

		capabilitiesService = new CapabilitiesTestService();
		let connectionProvider: azdata.ConnectionOption[] = [
			{
				name: 'connectionName',
				displayName: undefined,
				description: undefined,
				groupName: undefined,
				categoryValues: undefined,
				defaultValue: undefined,
				isIdentity: true,
				isRequired: true,
				specialValueType: ConnectionOptionSpecialType.connectionName,
				valueType: ServiceOptionType.string
			},
			{
				name: 'serverName',
				displayName: undefined,
				description: undefined,
				groupName: undefined,
				categoryValues: undefined,
				defaultValue: undefined,
				isIdentity: true,
				isRequired: true,
				specialValueType: ConnectionOptionSpecialType.serverName,
				valueType: ServiceOptionType.string
			},
			{
				name: 'databaseName',
				displayName: undefined,
				description: undefined,
				groupName: undefined,
				categoryValues: undefined,
				defaultValue: undefined,
				isIdentity: true,
				isRequired: true,
				specialValueType: ConnectionOptionSpecialType.databaseName,
				valueType: ServiceOptionType.string
			},
			{
				name: 'userName',
				displayName: undefined,
				description: undefined,
				groupName: undefined,
				categoryValues: undefined,
				defaultValue: undefined,
				isIdentity: true,
				isRequired: true,
				specialValueType: ConnectionOptionSpecialType.userName,
				valueType: ServiceOptionType.string
			},
			{
				name: 'authenticationType',
				displayName: undefined,
				description: undefined,
				groupName: undefined,
				categoryValues: undefined,
				defaultValue: undefined,
				isIdentity: true,
				isRequired: true,
				specialValueType: ConnectionOptionSpecialType.authType,
				valueType: ServiceOptionType.string
			},
			{
				name: 'password',
				displayName: undefined,
				description: undefined,
				groupName: undefined,
				categoryValues: undefined,
				defaultValue: undefined,
				isIdentity: true,
				isRequired: true,
				specialValueType: ConnectionOptionSpecialType.password,
				valueType: ServiceOptionType.string
			}
		];
		msSQLCapabilities = {
			providerId: 'MSSQL',
			displayName: 'MSSQL',
			connectionOptions: connectionProvider
		};

		provider2Capabilities = {
			providerId: 'MSSQL',
			displayName: 'MSSQL',
			connectionOptions: connectionProvider
		};
		capabilitiesService.capabilities['MSSQL'] = { connection: msSQLCapabilities };
		capabilitiesService.capabilities['Provider2'] = { connection: provider2Capabilities };
		let groups: IConnectionProfileGroup[] = [
			{
				id: 'root',
				name: 'root',
				parentId: '',
				color: '',
				description: ''
			},
			{
				id: 'g1',
				name: 'g1',
				parentId: 'root',
				color: 'blue',
				description: 'g1'
			}
		];
		connectionConfig.setup(x => x.getAllGroups()).returns(() => groups);

		defaultNamedConnectionProfile = new ConnectionProfile(capabilitiesService, defaultNamedProfile);
	});

	test('addActiveConnection should limit recent connection saves to the MaxRecentConnections amount', async () => {
		// Given 5 is the max # creds
		let numCreds = 6;

		// setup memento for MRU to return a list we have access to
		credentialStore.setup(x => x.saveCredential(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
			.returns(() => Promise.resolve(true));

		// When saving 4 connections
		// Expect all of them to be saved even if size is limited to 3
		let connectionStore = new ConnectionStore(context.object, workspaceConfigurationServiceMock.object,
			credentialStore.object, capabilitiesService, connectionConfig.object);
		for (let i = 0; i < numCreds; i++) {
			let cred = Object.assign({}, defaultNamedProfile, { serverName: defaultNamedProfile.serverName + i });
			let connectionProfile = new ConnectionProfile(capabilitiesService, cred);
			await connectionStore.addActiveConnection(connectionProfile, true);

			let current = connectionStore.getRecentlyUsedConnections();
			if (i >= maxRecent) {
				assert.equal(current.length, maxRecent, `expect only top ${maxRecent} creds to be saved to MRU`);
			} else {
				assert.equal(current.length, i + 1, `expect all credentials to be saved to MRU ${current.length}|${i + 1} `);
			}
			assert.equal(current[0].serverName, cred.serverName, 'Expect most recently saved item to be first in list');
			assert.ok(!current[0].password);
		}
		credentialStore.verify(x => x.saveCredential(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(numCreds));
		let recentConnections = connectionStore.getActiveConnections();
		assert.equal(numCreds, recentConnections.length, `expect number of active connection ${numCreds}|${recentConnections.length} `);
	});

	test('addActiveConnection with addToMru as false should not add any recent connections', async () => {
		// setup memento for MRU to return a list we have access to
		credentialStore.setup(x => x.saveCredential(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
			.returns(() => Promise.resolve(true));

		const numCreds = 3;
		let connectionStore = new ConnectionStore(context.object, workspaceConfigurationServiceMock.object,
			credentialStore.object, capabilitiesService, connectionConfig.object);
		connectionStore.clearActiveConnections();
		connectionStore.clearRecentlyUsed();

		for (let i = 0; i < 3; i++) {
			let cred = Object.assign({}, defaultNamedProfile, { serverName: defaultNamedProfile.serverName + i });
			let connectionProfile = new ConnectionProfile(capabilitiesService, cred);
			await connectionStore.addActiveConnection(connectionProfile, false);

			let recentConnections = connectionStore.getRecentlyUsedConnections();
			let activeConnections = connectionStore.getActiveConnections();
			assert.equal(recentConnections.length, 0, `expect no entries to be saved to MRU`);
			assert.equal(activeConnections.length, i + 1, `expect all credentials to be saved to activeConnections ${activeConnections.length}|${i + 1} `);
		}
		credentialStore.verify(x => x.saveCredential(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(numCreds));
	});

	test('getRecentlyUsedConnections should return connection for given provider', () => {
		let connectionStore = new ConnectionStore(context.object, workspaceConfigurationServiceMock.object,
			credentialStore.object, capabilitiesService, connectionConfig.object);
		let connections = connectionStore.getRecentlyUsedConnections(['Provider2']);
		assert.notEqual(connections, undefined);
		assert.equal(connections.every(c => c.providerName === 'Provider2'), true);
	});

	test('addActiveConnection should add same connection exactly once', async () => {
		// setup memento for MRU to return a list we have access to
		credentialStore.setup(x => x.saveCredential(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
			.returns(() => Promise.resolve(true));

		// Given we save the same connection twice
		// Then expect the only 1 instance of that connection to be listed in the MRU
		let connectionStore = new ConnectionStore(context.object, workspaceConfigurationServiceMock.object,
			credentialStore.object, capabilitiesService, connectionConfig.object);
		connectionStore.clearActiveConnections();
		connectionStore.clearRecentlyUsed();
		let cred = Object.assign({}, defaultNamedProfile, { serverName: defaultNamedProfile.serverName + 1 });
		let connectionProfile = new ConnectionProfile(capabilitiesService, cred);
		await connectionStore.addActiveConnection(defaultNamedConnectionProfile, true);
		await connectionStore.addActiveConnection(connectionProfile, true);
		await connectionStore.addActiveConnection(connectionProfile, true);

		let recentConnections = connectionStore.getRecentlyUsedConnections();
		assert.equal(recentConnections.length, 2, 'expect 2 unique credentials to have been added');
		assert.equal(recentConnections[0].serverName, cred.serverName, 'Expect most recently saved item to be first in list');
		assert.ok(!recentConnections[0].password);
	});

	test('addActiveConnection should save password to credential store', async () => {
		// Setup credential store to capture credentials sent to it
		let capturedCreds: any;
		credentialStore.setup(x => x.saveCredential(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
			.callback((cred: string, pass: any) => {
				capturedCreds = {
					'credentialId': cred,
					'password': pass
				};
			})
			.returns(() => Promise.resolve(true));

		// Given we save 1 connection with password and multiple other connections without
		let connectionStore = new ConnectionStore(context.object, workspaceConfigurationServiceMock.object,
			credentialStore.object, capabilitiesService, connectionConfig.object);
		connectionStore.clearActiveConnections();
		connectionStore.clearRecentlyUsed();

		let integratedCred = Object.assign({}, defaultNamedProfile, {
			serverName: defaultNamedProfile.serverName + 'Integrated',
			authenticationType: 'Integrated',
			userName: '',
			password: ''
		});
		let noPwdCred = Object.assign({}, defaultNamedProfile, {
			serverName: defaultNamedProfile.serverName + 'NoPwd',
			password: ''
		});
		let connectionProfile = new ConnectionProfile(capabilitiesService, defaultNamedProfile);

		let expectedCredCount = 0;
		expectedCredCount++;
		// Connection with stored password
		await connectionStore.addActiveConnection(connectionProfile, true);
		let recentConnections = connectionStore.getActiveConnections();

		// Then verify that saveCredential was called and correctly stored the password
		credentialStore.verify(x => x.saveCredential(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
		assert.strictEqual(capturedCreds.password, defaultNamedProfile.password);
		let credId: string = capturedCreds.credentialId;
		assert.ok(credId.includes(ConnectionStore.CRED_PROFILE_USER), 'Expect credential to be marked as an Profile cred');
		assert.ok(!recentConnections[0].password);

		// Integrated auth
		expectedCredCount++;
		let integratedCredConnectionProfile = new ConnectionProfile(capabilitiesService, integratedCred);
		await connectionStore.addActiveConnection(integratedCredConnectionProfile, true);

		recentConnections = connectionStore.getActiveConnections();
		// We shouldn't see an increase in the calls to saveCredential, but the MRU should be increased
		credentialStore.verify(x => x.saveCredential(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
		assert.equal(recentConnections.length, expectedCredCount, `expect ${expectedCredCount} unique credentials to have been added`);

		// Connection with blank (no) password
		expectedCredCount++;
		let noPwdCredConnectionProfile = new ConnectionProfile(capabilitiesService, noPwdCred);
		await connectionStore.addActiveConnection(noPwdCredConnectionProfile, true);

		recentConnections = connectionStore.getActiveConnections();
		// We shouldn't see an increase in the calls to saveCredential, but the MRU should be increased
		credentialStore.verify(x => x.saveCredential(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
		assert.equal(recentConnections.length, expectedCredCount, `expect ${expectedCredCount} unique credentials to have been added`);

	});

	test('can clear connections list', (done) => {
		connectionConfig.setup(x => x.getConnections(TypeMoq.It.isAny())).returns(() => []);

		let connectionStore = new ConnectionStore(context.object, workspaceConfigurationServiceMock.object,
			credentialStore.object, capabilitiesService, connectionConfig.object);

		// When we clear the connections list and get the list of available connection items
		connectionStore.clearActiveConnections();
		connectionStore.clearRecentlyUsed();
		// Expect no connection items
		let result = connectionStore.getActiveConnections();
		let expectedCount = 0; // 1 for create connection profile
		assert.equal(result.length, expectedCount);
		result = connectionStore.getRecentlyUsedConnections();
		assert.equal(result.length, expectedCount);
		// Then test is complete
		done();
	});

	test('isPasswordRequired should return true for MSSQL SqlLogin', () => {

		let connectionStore = new ConnectionStore(context.object, workspaceConfigurationServiceMock.object,
			credentialStore.object, capabilitiesService, connectionConfig.object);

		let expected: boolean = true;
		let actual = connectionStore.isPasswordRequired(defaultNamedProfile);

		assert.equal(expected, actual);
	});

	test('isPasswordRequired should return true for MSSQL SqlLogin for connection profile object', () => {
		let connectionStore = new ConnectionStore(context.object, workspaceConfigurationServiceMock.object,
			credentialStore.object, capabilitiesService, connectionConfig.object);
		let connectionProfile = new ConnectionProfile(capabilitiesService, defaultNamedProfile);
		let expected: boolean = true;
		let actual = connectionStore.isPasswordRequired(connectionProfile);

		assert.equal(expected, actual);
	});

	test('isPasswordRequired should return false if the password is not required in capabilities', () => {
		let providerName: string = 'providername';
		let connectionProvider = msSQLCapabilities.connectionOptions.map(o => {
			if (o.name === 'password') {
				o.isRequired = false;
			}
			return o;
		});
		let providerCapabilities = {
			providerId: providerName,
			displayName: providerName,
			connectionOptions: connectionProvider
		};

		capabilitiesService.capabilities[providerName] = { connection: providerCapabilities };

		let connectionStore = new ConnectionStore(context.object, workspaceConfigurationServiceMock.object,
			credentialStore.object, capabilitiesService, connectionConfig.object);
		let connectionProfile: IConnectionProfile = Object.assign({}, defaultNamedProfile, { providerName: providerName });
		let expected: boolean = false;
		let actual = connectionStore.isPasswordRequired(connectionProfile);

		assert.equal(expected, actual);
	});

	test('saveProfile should save the password after the profile is saved', done => {
		let password: string = 'asdf!@#$';
		let groupId: string = 'group id';
		let connectionProfile: IConnectionProfile = Object.assign({}, defaultNamedProfile, { password: password });
		let savedConnection: IConnectionProfile = Object.assign({}, connectionProfile, { groupId: groupId, password: '' });
		connectionConfig.setup(x => x.addConnection(TypeMoq.It.isAny())).returns(() => Promise.resolve(savedConnection));
		credentialStore.setup(x => x.saveCredential(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(true));

		let connectionStore = new ConnectionStore(context.object, workspaceConfigurationServiceMock.object,
			credentialStore.object, capabilitiesService, connectionConfig.object);

		connectionStore.saveProfile(connectionProfile).then(profile => {
			// add connection should be called with a profile without password
			connectionConfig.verify(x => x.addConnection(TypeMoq.It.is<IConnectionProfile>(c => c.password === '')), TypeMoq.Times.once());
			credentialStore.verify(x => x.saveCredential(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
			assert.equal(profile.password, password, 'The returned profile should still keep the password');
			assert.equal(profile.groupId, groupId, 'Group id should be set in the profile');
			done();
		}).catch(err => {
			assert.fail(err);
			done(err);
		});
	});

	test('addConnectionToMemento should not add duplicate items', () => {
		let connectionStore = new ConnectionStore(context.object, workspaceConfigurationServiceMock.object,
			credentialStore.object, capabilitiesService, connectionConfig.object);
		let mementoKey = 'RECENT_CONNECTIONS2';
		connectionStore.clearFromMemento(mementoKey);
		let connectionProfile: IConnectionProfile = Object.assign({}, defaultNamedProfile);
		connectionStore.addConnectionToMemento(connectionProfile, mementoKey);

		connectionProfile = Object.assign({}, defaultNamedProfile, { authenticationType: 'Integrated', userName: '' });
		connectionStore.addConnectionToMemento(connectionProfile, mementoKey);

		let currentList = connectionStore.getConnectionsFromMemento(mementoKey);
		assert.equal(currentList.length, 2, 'Adding same connection with different auth');

		connectionProfile = Object.assign({}, defaultNamedProfile, { groupFullName: 'new group' });
		connectionStore.addConnectionToMemento(connectionProfile, mementoKey);

		currentList = connectionStore.getConnectionsFromMemento(mementoKey);
		assert.equal(currentList.length, 3, 'Adding same connection with different group name');

		connectionProfile = Object.assign({}, defaultNamedProfile,
			{ groupFullName: defaultNamedProfile.groupFullName.toUpperCase() });
		connectionStore.addConnectionToMemento(connectionProfile, mementoKey);

		currentList = connectionStore.getConnectionsFromMemento(mementoKey);
		assert.equal(currentList.length, 3, 'Adding same connection with same group name but uppercase');

		connectionProfile = Object.assign({}, defaultNamedProfile,
			{ groupFullName: '' });
		connectionStore.addConnectionToMemento(connectionProfile, mementoKey);

		currentList = connectionStore.getConnectionsFromMemento(mementoKey);
		assert.equal(currentList.length, 3, 'Adding same connection with group empty string');

		connectionProfile = Object.assign({}, defaultNamedProfile,
			{ groupFullName: '/' });
		connectionStore.addConnectionToMemento(connectionProfile, mementoKey);

		currentList = connectionStore.getConnectionsFromMemento(mementoKey);
		assert.equal(currentList.length, 3, 'Adding same connection with group /');
	});

	test('getGroupFromId returns undefined when there is no group with the given ID', () => {
		let connectionStore = new ConnectionStore(context.object, workspaceConfigurationServiceMock.object,
			credentialStore.object, capabilitiesService, connectionConfig.object);
		let group = connectionStore.getGroupFromId('invalidId');
		assert.equal(group, undefined, 'Returned group was not undefined when there was no group with the given ID');
	});

	test('getGroupFromId returns the group that has the given ID', () => {
		// Set up the server groups with an additional group that contains a child group
		let groups: IConnectionProfileGroup[] = connectionConfig.object.getAllGroups();
		let parentGroupId = 'parentGroup';
		let childGroupId = 'childGroup';
		let parentGroup = new ConnectionProfileGroup(parentGroupId, undefined, parentGroupId, '', '');
		let childGroup = new ConnectionProfileGroup(childGroupId, parentGroup, childGroupId, '', '');
		groups.push(parentGroup, childGroup);
		let newConnectionConfig = TypeMoq.Mock.ofType(ConnectionConfig);
		newConnectionConfig.setup(x => x.getAllGroups()).returns(() => groups);
		let connectionStore = new ConnectionStore(context.object, workspaceConfigurationServiceMock.object,
			credentialStore.object, capabilitiesService, newConnectionConfig.object);

		// If I look up the parent group using its ID, then I get back the correct group
		let actualGroup = connectionStore.getGroupFromId(parentGroupId);
		assert.equal(actualGroup.id, parentGroupId, 'Did not get the parent group when looking it up with its ID');

		// If I look up the child group using its ID, then I get back the correct group
		actualGroup = connectionStore.getGroupFromId(childGroupId);
		assert.equal(actualGroup.id, childGroupId, 'Did not get the child group when looking it up with its ID');
	});

	test('getProfileWithoutPassword can return the profile without credentials in the password property or options dictionary', () => {
		let connectionStore = new ConnectionStore(context.object, workspaceConfigurationServiceMock.object,
			credentialStore.object, capabilitiesService, connectionConfig.object);
		let profile = Object.assign({}, defaultNamedProfile);
		profile.options['password'] = profile.password;
		profile.id = 'testId';
		let expectedProfile = Object.assign({}, profile);
		expectedProfile.password = '';
		expectedProfile.options['password'] = '';
		expectedProfile = ConnectionProfile.fromIConnectionProfile(capabilitiesService, expectedProfile).toIConnectionProfile();
		let profileWithoutCredentials = connectionStore.getProfileWithoutPassword(profile);
		assert.deepEqual(profileWithoutCredentials.toIConnectionProfile(), expectedProfile);
	});
});
