package org.teiid.services;

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.login.LoginException;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
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.OpenIDMetadata;
import org.teiid.adminapi.impl.SessionMetadata;
import org.teiid.adminapi.impl.VDBMetaData;
import org.teiid.auth.openid.OpenIDConnectManager;
import org.teiid.deployers.VDBRepository;
import org.teiid.dqp.service.SessionServiceException;
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.SecurityHelper;
import org.teiid.security.TeiidLoginContext;
import org.wildfly.security.auth.realm.SimpleMapBackedSecurityRealm;
import org.wildfly.security.auth.server.SecurityDomain;

@SuppressWarnings("nls")
public class TestSessionServiceImpl {
	SessionServiceImpl ssi;
	@BeforeEach
	public void setup() {
		ssi = new SessionServiceImpl();
		ssi.setSecurityHelper(new DoNothingSecurityHelper());
	}

	@AfterEach
	public void clean() {
		OpenIDConnectManager.TEST = false;
	}
	
	@Test
	public void testActiveVDBWithNoVersion() throws Exception {
		VDBRepository repo = Mockito.mock(VDBRepository.class);
		VDBMetaData vdb = new VDBMetaData();
		vdb.setName("name");
		vdb.setVersion(1);
		vdb.setStatus(Status.ACTIVE);
		
		
		Mockito.when(repo.getLiveVDB("name")).thenReturn(vdb);
		
		ssi.setVDBRepository(repo);
		
		ssi.getActiveVDB("name", null);
		
		Mockito.verify(repo, Mockito.times(1)).getLiveVDB("name");
	}
	
	@Test
	public void testActiveVDBWithVersion() throws Exception {
		VDBRepository repo = Mockito.mock(VDBRepository.class);
		VDBMetaData vdb = new VDBMetaData();
		vdb.setName("name");
		vdb.setVersion(1);
		vdb.setStatus(Status.ACTIVE);
		
		
		Mockito.when(repo.getLiveVDB("name", 1)).thenReturn(vdb);
		
		ssi.setVDBRepository(repo);
		
		ssi.getActiveVDB("name", "1");
		
		Mockito.verify(repo, Mockito.times(1)).getLiveVDB("name", 1);
	}
	
	
	@Test
	public void testActiveVDBNameWithVersion() throws Exception {
		VDBRepository repo = Mockito.mock(VDBRepository.class);
		VDBMetaData vdb = new VDBMetaData();
		vdb.setName("name");
		vdb.setVersion(1);
		vdb.setStatus(Status.ACTIVE);
		
		
		Mockito.when(repo.getLiveVDB("name", 1)).thenReturn(vdb);
		
		ssi.setVDBRepository(repo);
		
		ssi.getActiveVDB("name.1", null);
		
		Mockito.verify(repo, Mockito.times(1)).getLiveVDB("name", 1);
	}
	
	@Test
	public void testActiveVDBNameWithVersionNonInteger() throws Exception {
		VDBRepository repo = Mockito.mock(VDBRepository.class);
		VDBMetaData vdb = new VDBMetaData();
		vdb.setName("name");
		vdb.setVersion(1);
		vdb.setStatus(Status.ACTIVE);
		
		
		Mockito.when(repo.getLiveVDB("name", 1)).thenReturn(vdb);
		
		ssi.setVDBRepository(repo);
		
		try {
			ssi.getActiveVDB("name.x", null);
			fail("must have failed with non integer version");
		} catch (SessionServiceException e) {
		}
	}
	
	@Test
	public void testActiveVDBNameWithVersionAndVersion() throws Exception {
		VDBRepository repo = Mockito.mock(VDBRepository.class);
		VDBMetaData vdb = new VDBMetaData();
		vdb.setName("name");
		vdb.setVersion(1);
		vdb.setStatus(Status.ACTIVE);
		
		
		Mockito.when(repo.getLiveVDB("name", 1)).thenReturn(vdb);
		
		ssi.setVDBRepository(repo);
		
		try {
			ssi.getActiveVDB("name.1", "1");
			fail("must have failed with ambigious version info");
		} catch (SessionServiceException e) {
		}
		
		try {
			ssi.getActiveVDB("name..1", null);
			fail("must have failed with ambigious version info");
		} catch (SessionServiceException e) {
		}
	}	
	
	@Test public void testSecurityDomain() throws Exception {
		VDBRepository repo = Mockito.mock(VDBRepository.class);
		VDBMetaData vdb = new VDBMetaData();
		vdb.setName("name");
		vdb.setVersion(1);
		vdb.setStatus(Status.ACTIVE);
		vdb.addProperty(SessionServiceImpl.SECURITY_DOMAIN_PROPERTY, "domain");

		Mockito.when(repo.getVdbRepo()).thenReturn(new ConcurrentSkipListMap<>());
		Mockito.when(repo.getLiveVDB("name", 1)).thenReturn(vdb);

		ssi.setVDBRepository(repo);

		Properties properties = new Properties();
		properties.setProperty(TeiidURL.JDBC.VDB_NAME, "name.1");
		SessionMetadata s = ssi.createSession("name", "1", AuthenticationType.USERPASSWORD, "x", new Credentials(new char[] {'y'}), "z", properties);
		assertEquals("domain", s.getSecurityDomain());
	}

	@Test public void testLegacySecurityDomain() throws Exception {
		VDBRepository repo = Mockito.mock(VDBRepository.class);
		VDBMetaData vdb = new VDBMetaData();
		vdb.setName("name");
		vdb.setVersion(1);
		vdb.setStatus(Status.ACTIVE);

		Mockito.when(repo.getLiveVDB("name", 1)).thenReturn(vdb);
		Mockito.when(repo.getVdbRepo()).thenReturn(new ConcurrentSkipListMap<>());

		ssi.setVDBRepository(repo);
		ssi.setSecurityDomain("sd1,sd");
		Properties properties = new Properties();
		properties.setProperty(TeiidURL.JDBC.VDB_NAME, "name.1");
		SessionMetadata s = ssi.createSession("name", "1", AuthenticationType.USERPASSWORD, "x@sd", new Credentials(new char[] {'y'}), "z", properties);
		assertEquals("sd", s.getSecurityDomain());

		s = ssi.createSession("name", "1", AuthenticationType.USERPASSWORD, "x", new Credentials(new char[] {'y'}), "z", properties);
		assertEquals("sd1", s.getSecurityDomain());

		try {
			ssi.createSession("name", "1", AuthenticationType.USERPASSWORD, "x@sd2", new Credentials(new char[] {'y'}), "z", properties);
		} catch (LoginException e) {

		}

		ssi.setSecurityDomain("sd");
		s = ssi.createSession("name", "1", AuthenticationType.USERPASSWORD, "x@sd", new Credentials(new char[] {'y'}), "z", properties);
		assertEquals("sd", s.getSecurityDomain());

		vdb.addProperty(SessionServiceImpl.SECURITY_DOMAIN_PROPERTY, "domain");

		try {
			//conflict
			ssi.createSession("name", "1", AuthenticationType.USERPASSWORD, "x@sd", new Credentials(new char[] {'y'}), "z", properties);
		} catch (LoginException e) {

		}

	}

	@Test public void testAuthenticationType() throws Exception {
		// this is same as "domain/ANY"
		VDBRepository repo = Mockito.mock(VDBRepository.class);
		VDBMetaData vdb = new VDBMetaData();
		vdb.setName("name");
		vdb.setVersion(1);
		vdb.setStatus(Status.ACTIVE);
		vdb.addProperty(SessionServiceImpl.SECURITY_DOMAIN_PROPERTY, "domain");
		vdb.addProperty(SessionServiceImpl.GSS_PATTERN_PROPERTY, "x");
		Mockito.when(repo.getLiveVDB("name", 1)).thenReturn(vdb);

		ssi.setVDBRepository(repo);
		ssi.setAuthenticationType(AuthenticationType.USERPASSWORD); // this is transport default

		assertEquals(AuthenticationType.GSS, ssi.getAuthenticationType("name", "1", "x"));
		assertEquals(AuthenticationType.USERPASSWORD, ssi.getAuthenticationType("name", "1", "y"));
		assertEquals(AuthenticationType.USERPASSWORD, ssi.getAuthenticationType("name", "1", "z"));

		// testing specific domain, enforcing
		vdb = new VDBMetaData();
		vdb.setName("name1");
		vdb.setVersion(1);
		vdb.setStatus(Status.ACTIVE);
		vdb.addProperty(SessionServiceImpl.SECURITY_DOMAIN_PROPERTY, "domain");
		vdb.addProperty(SessionServiceImpl.AUTHENTICATION_TYPE_PROPERTY, "GSS");
		Mockito.when(repo.getLiveVDB("name1", 1)).thenReturn(vdb);

		assertEquals(AuthenticationType.GSS, ssi.getAuthenticationType("name1", "1", "x"));
		assertEquals(AuthenticationType.GSS, ssi.getAuthenticationType("name1", "1", "y"));

		// testing transport default
		vdb = new VDBMetaData();
		vdb.setName("name2");
		vdb.setVersion(1);
		vdb.setStatus(Status.ACTIVE);

		Mockito.when(repo.getLiveVDB("name2", 1)).thenReturn(vdb);

		assertEquals(AuthenticationType.USERPASSWORD, ssi.getAuthenticationType("name2", "1", "x"));
		assertEquals(AuthenticationType.USERPASSWORD, ssi.getAuthenticationType("name2", "1", "y"));

		ssi.setAuthenticationType(AuthenticationType.GSS); // this is transport default
		assertEquals(AuthenticationType.GSS, ssi.getAuthenticationType("name2", "1", "x"));
		assertEquals(AuthenticationType.GSS, ssi.getAuthenticationType("name2", "1", "y"));

	}

    @Test public void testBaseUsername() throws Exception {

        assertEquals("foo@bar.com", SessionServiceImpl.getBaseUsername("foo\\@bar.com@foo")); //$NON-NLS-1$ //$NON-NLS-2$
        
        assertEquals("foo", SessionServiceImpl.getDomainName("me\\@bar.com@foo", AuthenticationType.USERPASSWORD)); //$NON-NLS-1$ //$NON-NLS-2$
        
        assertEquals(null, SessionServiceImpl.getDomainName("@", AuthenticationType.USERPASSWORD)); //$NON-NLS-1$
        
        assertEquals("@", SessionServiceImpl.getBaseUsername("@")); //$NON-NLS-1$ //$NON-NLS-2$

	    assertEquals("SSO", SessionServiceImpl.getDomainName("test@bar.com", AuthenticationType.OPENID));

	    assertEquals("SSO", SessionServiceImpl.getDomainName("test@bar.com", AuthenticationType.OAUTH2));
    }

	@Test public void testOpenId() throws Exception {
		final String securityDomain = "SSO";
		final String baseUserName = "test";
		final String baseUserName2 = "test2";
		Credentials credentials = new Credentials(baseUserName.toCharArray());
		VDBRepository repo = Mockito.mock(VDBRepository.class);
		VDBMetaData vdb = new VDBMetaData();
		vdb.setName("name");
		vdb.setVersion(1);
		vdb.setStatus(Status.ACTIVE);
		vdb.addProperty(SessionServiceImpl.SECURITY_DOMAIN_PROPERTY, securityDomain);

		Mockito.when(repo.getVdbRepo()).thenReturn(new ConcurrentSkipListMap<>());
		Mockito.when(repo.getLiveVDB("name", 1)).thenReturn(vdb);

		ssi.setVDBRepository(repo);

		Properties properties = new Properties();
		properties.setProperty(TeiidURL.JDBC.VDB_NAME, "name.1");

		OpenIDConnectManager openIDConnectManager = Mockito.mock(OpenIDConnectManager.class);
		stubOpenIdAuthenticate(openIDConnectManager, baseUserName, securityDomain);
		assertEquals(0, ssi.getActiveOpenIdCount());
		SessionMetadata s1 = ssi.createSession("name", "1", AuthenticationType.OPENID, baseUserName,
				credentials, "z", properties);
		assertEquals(1, ssi.getActiveOpenIdCount());
		assertEquals(securityDomain, s1.getSecurityDomain());
		// imitate change of tokens
		OpenIDMetadata openIDMetadata_s1 = s1.getOpenIDMetadata();
		openIDMetadata_s1.setAccessToken("accessToken1");
		openIDMetadata_s1.setRefreshToken("refreshToken1");

		SessionMetadata s1_1 = ssi.createSession("name", "1", AuthenticationType.OPENID, baseUserName,
				credentials, "z", properties);
		// for sessions with the same username we must have the same OpenID metadata object
		assertEquals(1, ssi.getActiveOpenIdCount());
		assertEquals(securityDomain, s1_1.getSecurityDomain());
		OpenIDMetadata openIDMetadata_s1_1 = s1_1.getOpenIDMetadata();
		assertEquals(null, openIDMetadata_s1_1.getAccessToken());
		assertEquals(null, openIDMetadata_s1_1.getRefreshToken());

		// tokens for s1 session were updated by values from s1_1 session
		assertEquals(null, openIDMetadata_s1.getAccessToken());
		assertEquals(null, openIDMetadata_s1.getRefreshToken());

		stubOpenIdAuthenticate(openIDConnectManager, baseUserName2, securityDomain);
		SessionMetadata s2 = ssi.createSession("name", "1", AuthenticationType.OPENID, baseUserName2,
				credentials, "z", properties);
		assertEquals(2, ssi.getActiveOpenIdCount());
		assertEquals(securityDomain, s2.getSecurityDomain());

		ssi.closeSession(s2.getSessionId());
		assertEquals(1, ssi.getActiveOpenIdCount());

		ssi.closeSession(s1_1.getSessionId());
		assertEquals(1, ssi.getActiveOpenIdCount());

		ssi.closeSession(s1.getSessionId());
		assertEquals(0, ssi.getActiveOpenIdCount());
	}

	@Test public void testOpenIdAuthServerUrl() throws Exception {
		assertEquals(null, OpenIDConnectManager.getAuthServerUrl(""));
		assertEquals("http://localhost", OpenIDConnectManager.getAuthServerUrl("http://localhost"));
		assertEquals("http://localhost:8081", OpenIDConnectManager.getAuthServerUrl("http://localhost:8081"));
		assertEquals("https://localhost:8081", OpenIDConnectManager.getAuthServerUrl("https://localhost:8081"));
		assertEquals("http://localhost:8081", OpenIDConnectManager.getAuthServerUrl("http://localhost:8081/realms/SSO/.well-known/openid-configuration"));
		assertEquals("http://localhost:8081/auth", OpenIDConnectManager.getAuthServerUrl("http://localhost:8081/auth/realms/SSO/.well-known/openid-configuration"));
	}

	@Test public void testOpenIdConnectionUrl() throws Exception {
		final String TEST_USERNAME = "testUserName";
		OpenIDConnectManager openIDConnectManager = new OpenIDConnectManager();
		OpenIDConnectManager.TEST = true;
		Properties props = getConnectionProperties();

		OpenIDMetadata openIDMetadata = new OpenIDMetadata();
		openIDMetadata.setConfigUrl("metadataUrl");
		openIDMetadata.setClientId("metadataClientId");
		openIDMetadata.setClientSecret("metadataClientSecret");
		openIDConnectManager.authenticate(TEST_USERNAME, null, props, null, null, openIDMetadata);

		// url/clientId/clientSecret must be taken only from OpenIDMetadata, not from connection url properties
		Assertions.assertTrue(openIDMetadata.getConfigUrl().equals("metadataUrl"));
		Assertions.assertTrue(openIDMetadata.getClientId().equals("metadataClientId"));
		Assertions.assertTrue(openIDMetadata.getClientSecret().equals("metadataClientSecret"));
		Assertions.assertTrue(openIDMetadata.getProvider().equals("provider"));
		Assertions.assertTrue(openIDMetadata.getRealm().equals("testRealm"));
		Assertions.assertTrue(openIDMetadata.getAccessToken() == null);
		Assertions.assertTrue(openIDMetadata.getRefreshToken().equals("refreshToken"));
		Assertions.assertTrue(openIDMetadata.isUseDvRoles());
	}

	private Properties getConnectionProperties() {
		Properties props = new Properties();
		props.put(OpenIDConnectManager.PROP_CONFIGURATION_URL, "connectionUrl");
		props.put(OpenIDConnectManager.PROP_CLIENT_ID, "connectionClientId");
		props.put(OpenIDConnectManager.PROP_CLIENT_SECRET, "connectionClientSecret");
		props.put(OpenIDConnectManager.PROP_EXPIRES_IN, "1000");
		props.put(OpenIDConnectManager.PROP_USE_DV_ROLES, "true");
		props.put(OpenIDConnectManager.PROP_PROVIDER, "provider");
		props.put(OpenIDConnectManager.PROP_REALM, "testRealm");
		props.put(TeiidURL.CONNECTION.REFRESH_TOKEN, "refreshToken");
		return props;
	}

	private void stubOpenIdAuthenticate(OpenIDConnectManager openIDConnectManager, String userName, String securityDomain ) throws Exception {
		SecurityDomain domain = SecurityDomain.builder()
				.addRealm("Simple", new SimpleMapBackedSecurityRealm()).build()
				.setDefaultRealmName("Simple")
				.build();
		Mockito.when(openIDConnectManager.authenticate(Mockito.anyString(), Mockito.any(Credentials.class),
				Mockito.any(Properties.class), Mockito.anyString(),
				Mockito.any(SecurityHelper.class), Mockito.any(OpenIDMetadata.class))).thenReturn(
				new TeiidLoginContext(userName+"@"+securityDomain, domain.createAdHocIdentity("test"), securityDomain)
		);
		ssi.setOpenIDConnectManager(openIDConnectManager);
	}
}
