/*
 * JBoss, Home of Professional Open Source.
 * Copyright (C) 2008 Red Hat, Inc.
 * Licensed to Red Hat, Inc. under one or more contributor 
 * license agreements.  See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 * Modified by
 * @author  Data Virtuality GmbH
 * @date    $Date$
 *  
 */
// https://issues.redhat.com/browse/TEIID-3663
package org.teiid.transport;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

import java.util.Properties;
import java.util.concurrent.ConcurrentSkipListMap;

import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.teiid.adminapi.VDB.Status;
import org.teiid.adminapi.impl.SessionMetadata;
import org.teiid.adminapi.impl.VDBMetaData;
import org.teiid.client.security.ILogon;
import org.teiid.client.security.LogonException;
import org.teiid.client.security.LogonResult;
import org.teiid.client.security.SessionToken;
import org.teiid.core.util.Base64;
import org.teiid.deployers.VDBRepository;
import org.teiid.dqp.internal.process.DQPWorkContext;
import org.teiid.dqp.service.SessionService;
import org.teiid.net.TeiidURL;
import org.teiid.net.socket.AuthenticationType;
import org.teiid.runtime.DoNothingSecurityHelper;
import org.teiid.security.Credentials;
import org.teiid.security.TeiidLoginContext;
import org.teiid.services.SessionServiceImpl;
import org.wildfly.security.auth.server.SecurityIdentity;

@SuppressWarnings("nls")
public class TestLogonImpl {
	SessionServiceImpl ssi;

	@BeforeEach
	public void setup() {
		ssi = new SessionServiceImpl();
		ssi.setSecurityHelper(new DoNothingSecurityHelper() {
		    @Override
            public TeiidLoginContext authenticate(String securityDomain, String userName, Credentials credentials,
                    String applicationName) throws LoginException {
                return new TeiidLoginContext(userName, createAdHocIdentity(securityDomain, userName, null), securityDomain);
            }
		});
	}

	@Test
	public void testLogonResult() throws Exception {
		SessionService ssi = Mockito.mock(SessionService.class);
		Mockito.when(ssi.getAuthenticationType(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(AuthenticationType.USERPASSWORD);
		DQPWorkContext.setWorkContext(new DQPWorkContext());
		String userName = "Fred"; //$NON-NLS-1$
		String applicationName = "test"; //$NON-NLS-1$
		Properties p = new Properties();
		p.setProperty(TeiidURL.CONNECTION.USER_NAME, userName);
		p.setProperty(TeiidURL.CONNECTION.APP_NAME, applicationName);
		p.setProperty(TeiidURL.JDBC.VDB_NAME, "x");
		p.setProperty(TeiidURL.JDBC.VDB_VERSION, "1");

		SessionMetadata session = new SessionMetadata();
		session.setUserName(userName);
		session.setApplicationName(applicationName);
		session.setSessionId(String.valueOf(1));
		session.setSessionToken(new SessionToken(1, userName));

		Mockito.when(ssi.createSession("x", "1", AuthenticationType.USERPASSWORD, userName, null, applicationName,p)).thenReturn(session); 

		LogonImpl impl = new LogonImpl(ssi, "fakeCluster"); //$NON-NLS-1$

		LogonResult result = impl.logon(p);
		assertEquals(userName, result.getUserName());
		assertEquals(String.valueOf(1), result.getSessionID());
	}
	
	@Test
	public void testLogonAuthenticationType() throws Exception {
		VDBRepository repo = Mockito.mock(VDBRepository.class);
		Mockito.when(repo.getVdbRepo()).thenReturn(new ConcurrentSkipListMap<>());
		VDBMetaData vdb = new VDBMetaData();
		vdb.addProperty(SessionServiceImpl.GSS_PATTERN_PROPERTY, "GSS");
		vdb.setName("name");
		vdb.setVersion(1);
		vdb.setStatus(Status.ACTIVE);
		Mockito.when(repo.getLiveVDB("name", 1)).thenReturn(vdb);

		ssi.setVDBRepository(repo);
		ssi.setSecurityDomain("SC");

		// default transport - what Teiid has before TEIID-2863
		ssi.setAuthenticationType(AuthenticationType.USERPASSWORD); // this is transport default
		DQPWorkContext.setWorkContext(new DQPWorkContext());
		Properties p = buildProperties("fred", "name");
		LogonImpl impl = new LogonImpl(ssi, "fakeCluster"); //$NON-NLS-1$
		LogonResult result = impl.logon(p);
		assertEquals("fred", result.getUserName());

		// if no preference then choose USERPASSWORD
		ssi.setAuthenticationType(AuthenticationType.USERPASSWORD); // this is transport default
		DQPWorkContext.setWorkContext(new DQPWorkContext());
		p = buildProperties("fred", "name");
		impl = new LogonImpl(ssi, "fakeCluster"); //$NON-NLS-1$
		result = impl.logon(p);
		assertEquals("fred", result.getUserName());

		// if user name is set to "GSS", then the preference is set to "GSS"
		SecurityIdentity si = ssi.getSecurityHelper().createAdHocIdentity("Simple", "GSS", null);
		si.runAs(() -> {
			ssi.setAuthenticationType(AuthenticationType.USERPASSWORD); // this is transport default
		DQPWorkContext.setWorkContext(new DQPWorkContext());
			Properties pGss = buildProperties("GSS", "name");
		FakeGssLogonImpl fimpl = new FakeGssLogonImpl(ssi, "fakeCluster"); //$NON-NLS-1$
			fimpl.addToken("bytes".getBytes(), si);
			pGss.put(ILogon.KRB5TOKEN, "bytes".getBytes());
			LogonResult resultGss;
			try {
				resultGss = fimpl.logon(pGss);
			} catch (LogonException e) {
				throw new RuntimeException(e);
			}
			assertEquals("GSS@SC", resultGss.getUserName());
		});

/*
		// if the transport default defined as GSS, then preference is USERPASSWORD, throw exception
		ssi.setAuthenticationType(AuthenticationType.GSS);
		try {
			DQPWorkContext.setWorkContext(new DQPWorkContext());
			p = buildProperties("fred", "name");
			impl = new LogonImpl(ssi, "fakeCluster"); //$NON-NLS-1$
			result = impl.logon(p);
			fail("should have failed due server does not support USERPASSWORD");
		} catch(LogonException e) {
			// pass
		}
*/
	}
	
	@Test
	public void testLogonAuthenticationTypeByVDB() throws Exception {
		VDBRepository repo = Mockito.mock(VDBRepository.class);
		Mockito.when(repo.getVdbRepo()).thenReturn(new ConcurrentSkipListMap<>());
		ssi.setVDBRepository(repo);

		// when VDB value is is avavailble this will not be used
		ssi.setAuthenticationType(AuthenticationType.GSS);

		// default transport - what Teiid has before TEIID-2863
		addVdb(repo, "name", "SC", AuthenticationType.USERPASSWORD.name());
		DQPWorkContext.setWorkContext(new DQPWorkContext());
		Properties p = buildProperties("fred", "name");
		LogonImpl impl = new LogonImpl(ssi, "fakeCluster"); //$NON-NLS-1$
		LogonResult result = impl.logon(p);
		assertEquals("fred", result.getUserName());

		// if no preference then choose USERPASSWORD
		VDBMetaData metadata = addVdb(repo, "name1", "SC", AuthenticationType.USERPASSWORD.name());
		metadata.addProperty(SessionServiceImpl.GSS_PATTERN_PROPERTY, "GSS");
		DQPWorkContext.setWorkContext(new DQPWorkContext());
		LogonImpl impl2 = new LogonImpl(ssi, "fakeCluster"); //$NON-NLS-1$
		p = buildProperties("fred", "name1");
		result = impl2.logon(p);
		assertEquals("fred", result.getUserName());

		SecurityIdentity si = ssi.getSecurityHelper().createAdHocIdentity("Simple", "GSS", null);
		si.runAs(() -> {
			Properties pGss = buildProperties("GSS", "name1");
		FakeGssLogonImpl fimpl = new FakeGssLogonImpl(ssi, "fakeCluster"); //$NON-NLS-1$
			fimpl.addToken("bytes".getBytes(), si);
			pGss.put(ILogon.KRB5TOKEN, "bytes".getBytes());
			LogonResult resultGss;
			try {
				resultGss = fimpl.logon(pGss);
			} catch (LogonException e) {
				throw new RuntimeException(e);
			}
			assertEquals("GSS@SC", resultGss.getUserName());


		// here preference is GSS
		try {
				pGss = buildProperties("GSS", "name");
				resultGss = impl2.logon(pGss);
				assertEquals("GSS", resultGss.getUserName());
			} catch (LogonException e) {

		}
		});

/*
		// if the transport default defined as GSS, then preference is USERPASSWORD, throw exception
		try {
			addVdb(repo, "name2", "SC", "GSS");
			DQPWorkContext.setWorkContext(new DQPWorkContext());
			impl2 = new LogonImpl(ssi, "fakeCluster"); //$NON-NLS-1$
			p = buildProperties("fred", "name2");
			result = impl2.logon(p);
			fail("should have failed due server does not support USERPASSWORD");
		} catch(LogonException e) {
			// pass
		}
*/
	}

	private Properties buildProperties(String userName, String vdbName) {
		Properties p = new Properties();
		p.setProperty(TeiidURL.CONNECTION.USER_NAME, userName);
		p.setProperty(TeiidURL.CONNECTION.APP_NAME, "test");
		p.setProperty(TeiidURL.JDBC.VDB_NAME, vdbName);
		p.setProperty(TeiidURL.JDBC.VDB_VERSION, "1");
		return p;
	}

	private VDBMetaData addVdb(VDBRepository repo, String name, String sc, String authenticationType) {
		VDBMetaData vdb = new VDBMetaData();
		vdb.setName(name);
		vdb.setVersion(1);
		vdb.setStatus(Status.ACTIVE);
		Mockito.when(repo.getLiveVDB(name, 1)).thenReturn(vdb);
		vdb.addProperty(SessionServiceImpl.SECURITY_DOMAIN_PROPERTY, sc);
		vdb.addProperty(SessionServiceImpl.AUTHENTICATION_TYPE_PROPERTY, authenticationType);
		return vdb;
	}

	class FakeGssLogonImpl extends LogonImpl {

		public FakeGssLogonImpl(SessionService service, String clusterName) {
			super(service, clusterName);
		}

		public void addToken(byte[] token, SecurityIdentity securityContext) {
			this.gssServiceTickets.put(Base64.encodeBytes(MD5(token)), securityContext);
		}
	}
}
