/**
 * This file is part of the Meeds project (https://meeds.io/).
 *
 * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
 *
 * This program 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 3 of the License, or (at your option) any later version.
 * This program 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 program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package io.meeds.oauth.test;

import java.lang.reflect.UndeclaredThrowableException;

import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse;

import org.exoplatform.component.test.AbstractKernelTest;
import org.exoplatform.component.test.ConfigurationUnit;
import org.exoplatform.component.test.ConfiguredBy;
import org.exoplatform.component.test.ContainerScope;
import org.exoplatform.services.organization.OrganizationService;
import org.exoplatform.services.organization.User;
import org.exoplatform.services.organization.UserProfile;
import org.exoplatform.services.organization.impl.UserImpl;
import org.exoplatform.web.security.codec.AbstractCodec;
import org.exoplatform.web.security.codec.CodecInitializer;

import io.meeds.oauth.common.OAuthConstants;
import io.meeds.oauth.exception.OAuthException;
import io.meeds.oauth.exception.OAuthExceptionCode;
import io.meeds.oauth.facebook.FacebookAccessTokenContext;
import io.meeds.oauth.google.GoogleAccessTokenContext;
import io.meeds.oauth.spi.OAuthProviderType;
import io.meeds.oauth.spi.OAuthProviderTypeRegistry;
import io.meeds.oauth.spi.SocialNetworkService;
import io.meeds.oauth.twitter.TwitterAccessTokenContext;

/**
 * Test persistence of data with usage of {@link SocialNetworkService}
 *
 */
@ConfiguredBy({
  @ConfigurationUnit(scope = ContainerScope.ROOT, path = "conf/configuration.xml"),
  @ConfigurationUnit(scope = ContainerScope.PORTAL, path = "conf/portal/configuration.xml"),
  @ConfigurationUnit(scope = ContainerScope.PORTAL, path = "conf/exo.portal.component.web.oauth-configuration-local.xml")
})
public class TestSocialNetworkService extends AbstractKernelTest {

  private OrganizationService       orgService;

  private SocialNetworkService      socialNetworkService;

  private OAuthProviderTypeRegistry oAuthProviderTypeRegistry;

  private AbstractCodec             codec;

  @Override
  public void setUp() throws Exception {
    super.setUp();
    this.oAuthProviderTypeRegistry = getContainer().getComponentInstanceOfType(OAuthProviderTypeRegistry.class);
    this.orgService = getContainer().getComponentInstanceOfType(OrganizationService.class);
    this.socialNetworkService = getContainer().getComponentInstanceOfType(SocialNetworkService.class);
    this.codec = getContainer().getComponentInstanceOfType(CodecInitializer.class).getCodec();
    begin();
  }

  @Override
  public void tearDown() throws Exception {
    end();
    super.tearDown();
  }

  public void testPersistOAuthProviderUsernames() throws Exception {
    User user1 = new UserImpl("testUser1");
    User user2 = new UserImpl("testUser2");
    orgService.getUserHandler().createUser(user1, false);
    orgService.getUserHandler().createUser(user2, false);

    // Save facebook username and google username for user1
    UserProfile userProfile1 = orgService.getUserProfileHandler().createUserProfileInstance(user1.getUserName());
    userProfile1.setAttribute(OAuthConstants.PROFILE_FACEBOOK_USERNAME, "joseph.doyle");
    userProfile1.setAttribute(OAuthConstants.PROFILE_GOOGLE_USERNAME, "joseph.something");
    orgService.getUserProfileHandler().saveUserProfile(userProfile1, true);

    // Save facebook username and google username for user2
    UserProfile userProfile2 = orgService.getUserProfileHandler().createUserProfileInstance(user2.getUserName());
    userProfile2.setAttribute(OAuthConstants.PROFILE_FACEBOOK_USERNAME, "john.doyle");
    userProfile2.setAttribute(OAuthConstants.PROFILE_GOOGLE_USERNAME, "john.something");
    orgService.getUserProfileHandler().saveUserProfile(userProfile2, true);

    // Find user by facebook and google username
    User foundUser = socialNetworkService.findUserByOAuthProviderUsername(getFacebookProvider(), "joseph.doyle");
    assertNotNull(foundUser);
    assertEquals(foundUser.getUserName(), user1.getUserName());

    User foundUser2 = socialNetworkService.findUserByOAuthProviderUsername(getFacebookProvider(), "john.doyle");
    assertNotNull(foundUser2);
    assertEquals(foundUser2.getUserName(), user2.getUserName());

    User foundUser3 = socialNetworkService.findUserByOAuthProviderUsername(getGoogleProvider(), "john.something");
    assertNotNull(foundUser3);
    assertEquals(foundUser3.getUserName(), user2.getUserName());

    // Try to change facebook username for user1 with socialNetworkService
    socialNetworkService.updateOAuthInfo(getFacebookProvider(),
                                         user1.getUserName(),
                                         "joseph.doyle.changed",
                                         createFacebookAccessToken("someToken"));

    User foundUser4 = socialNetworkService.findUserByOAuthProviderUsername(getFacebookProvider(), "joseph.doyle.changed");
    assertNotNull(foundUser4);
    assertEquals(foundUser4.getUserName(), user1.getUserName());

    try {
      // This should fail because of duplicated facebook username
      socialNetworkService.updateOAuthInfo(getFacebookProvider(),
                                           user2.getUserName(),
                                           "joseph.doyle.changed",
                                           createFacebookAccessToken("someToken"));

      fail("Exception should occur because of duplicated facebook username");
    } catch (OAuthException gtnOauthOAuthException) {
      assertEquals(OAuthExceptionCode.DUPLICATE_OAUTH_PROVIDER_USERNAME, gtnOauthOAuthException.getExceptionCode());
      assertEquals(OAuthConstants.PROFILE_FACEBOOK_USERNAME,
                   gtnOauthOAuthException.getExceptionAttribute(OAuthConstants.EXCEPTION_OAUTH_PROVIDER_USERNAME_ATTRIBUTE_NAME));
      assertEquals("joseph.doyle.changed",
                   gtnOauthOAuthException.getExceptionAttribute(OAuthConstants.EXCEPTION_OAUTH_PROVIDER_USERNAME));
    } catch (Exception e) {
      throw new UndeclaredThrowableException(e);
    }

    orgService.getUserHandler().removeUser(user1.getUserName(), false);
    orgService.getUserHandler().removeUser(user2.getUserName(), false);
  }

  public void testPersistOAuthAccessTokens() throws Exception {
    User user1 = new UserImpl("testUser1");
    User user2 = new UserImpl("testUser2");
    orgService.getUserHandler().createUser(user1, false);
    orgService.getUserHandler().createUser(user2, false);

    // Update some facebook accessTokens
    OAuthProviderType<FacebookAccessTokenContext> facebookProvider = getFacebookProvider();
    socialNetworkService.updateOAuthAccessToken(facebookProvider, user1.getUserName(), createFacebookAccessToken("aaa123"));
    socialNetworkService.updateOAuthAccessToken(facebookProvider, user2.getUserName(), createFacebookAccessToken("bbb456"));

    // Update some google accessToken
    GoogleAccessTokenContext googleToken = createGoogleAccessToken("ccc789", "rfrc487", "http://someScope");
    socialNetworkService.updateOAuthAccessToken(getGoogleProvider(), user1.getUserName(), googleToken);

    // Update some twitter accessToken
    TwitterAccessTokenContext twitterToken = new TwitterAccessTokenContext("tok1", "secret1");
    socialNetworkService.updateOAuthAccessToken(getTwitterProvider(), user1.getUserName(), twitterToken);

    // Verify that FB accessTokens could be obtained
    assertEquals("aaa123", socialNetworkService.getOAuthAccessToken(facebookProvider, user1.getUserName()).getAccessToken());
    assertEquals("bbb456", socialNetworkService.getOAuthAccessToken(facebookProvider, user2.getUserName()).getAccessToken());

    // Verify that Google accessToken could be obtained
    googleToken = createGoogleAccessToken("ccc789", "rfrc487", "http://someScope");
    assertEquals(googleToken, socialNetworkService.getOAuthAccessToken(getGoogleProvider(), user1.getUserName()));
    assertNull(socialNetworkService.getOAuthAccessToken(getGoogleProvider(), user2.getUserName()));

    // Verify that twitter accessToken could be obtained
    assertEquals(new TwitterAccessTokenContext("tok1", "secret1"),
                 socialNetworkService.getOAuthAccessToken(getTwitterProvider(), user1.getUserName()));
    assertNull(socialNetworkService.getOAuthAccessToken(getTwitterProvider(), user2.getUserName()));

    // Directly obtain accessTokens from userProfile and verify that they are
    // encoded
    UserProfile userProfile1 = orgService.getUserProfileHandler().findUserProfileByName("testUser1");
    UserProfile userProfile2 = orgService.getUserProfileHandler().findUserProfileByName("testUser2");
    String encodedAccessToken1 = userProfile1.getAttribute(OAuthConstants.PROFILE_FACEBOOK_ACCESS_TOKEN + ".1");
    String encodedAccessToken2 = userProfile2.getAttribute(OAuthConstants.PROFILE_FACEBOOK_ACCESS_TOKEN + ".1");
    assertFalse("aaa123".equals(encodedAccessToken1));
    assertFalse("bbb456".equals(encodedAccessToken2));
    assertTrue(codec.encode("aaa123").equals(encodedAccessToken1));
    assertTrue(codec.encode("bbb456").equals(encodedAccessToken2));

    // Verify that tokens are null after invalidation
    socialNetworkService.removeOAuthAccessToken(facebookProvider, user1.getUserName());
    socialNetworkService.removeOAuthAccessToken(getGoogleProvider(), user1.getUserName());
    socialNetworkService.removeOAuthAccessToken(getTwitterProvider(), user1.getUserName());
    assertNull(socialNetworkService.getOAuthAccessToken(facebookProvider, user1.getUserName()));
    assertNull(socialNetworkService.getOAuthAccessToken(getGoogleProvider(), user1.getUserName()));
    assertNull(socialNetworkService.getOAuthAccessToken(getTwitterProvider(), user1.getUserName()));
    assertNotNull(socialNetworkService.getOAuthAccessToken(facebookProvider, user2.getUserName()));

    orgService.getUserHandler().removeUser(user1.getUserName(), false);
    orgService.getUserHandler().removeUser(user2.getUserName(), false);
  }

  public void testInvalidationOfAccessTokens() throws Exception {
    User user1 = new UserImpl("testUser1");
    orgService.getUserHandler().createUser(user1, false);

    // Update some accessToken and verify that it's available
    socialNetworkService.updateOAuthInfo(getFacebookProvider(),
                                         user1.getUserName(),
                                         "fbUsername1",
                                         createFacebookAccessToken("fbAccessToken1"));
    assertEquals("fbAccessToken1",
                 socialNetworkService.getOAuthAccessToken(getFacebookProvider(), user1.getUserName()).getAccessToken());

    // Update some accessToken again
    socialNetworkService.updateOAuthInfo(getFacebookProvider(),
                                         user1.getUserName(),
                                         "fbUsername1",
                                         createFacebookAccessToken("fbAccessToken2"));
    assertEquals("fbAccessToken2",
                 socialNetworkService.getOAuthAccessToken(getFacebookProvider(), user1.getUserName()).getAccessToken());

    // Update userProfile and change FB username. AccessToken should be
    // invalidated
    UserProfile userProfile1 = orgService.getUserProfileHandler().findUserProfileByName(user1.getUserName());
    userProfile1.setAttribute(getFacebookProvider().getUserNameAttrName(), "fbUsername2");
    orgService.getUserProfileHandler().saveUserProfile(userProfile1, true);
    assertNull(socialNetworkService.getOAuthAccessToken(getFacebookProvider(), user1.getUserName()));

    // Update some accessToken and verify it's here now
    socialNetworkService.updateOAuthAccessToken(getFacebookProvider(),
                                                user1.getUserName(),
                                                createFacebookAccessToken("fbAccessToken3"));
    assertEquals("fbAccessToken3",
                 socialNetworkService.getOAuthAccessToken(getFacebookProvider(), user1.getUserName()).getAccessToken());

    // Null FB username and verify that accessToken is invalidated
    userProfile1 = orgService.getUserProfileHandler().findUserProfileByName(user1.getUserName());
    userProfile1.setAttribute(getFacebookProvider().getUserNameAttrName(), null);
    orgService.getUserProfileHandler().saveUserProfile(userProfile1, true);
    assertNull(socialNetworkService.getOAuthAccessToken(getFacebookProvider(), user1.getUserName()));

    // Test this with Twitter
    TwitterAccessTokenContext twitterToken = new TwitterAccessTokenContext("token1", "secret1");
    socialNetworkService.updateOAuthInfo(getTwitterProvider(), user1.getUserName(), "twitterUsername1", twitterToken);

    userProfile1 = orgService.getUserProfileHandler().findUserProfileByName(user1.getUserName());
    userProfile1.setAttribute(getTwitterProvider().getUserNameAttrName(), "twitterUsername2");
    orgService.getUserProfileHandler().saveUserProfile(userProfile1, true);
    assertNull(socialNetworkService.getOAuthAccessToken(getTwitterProvider(), user1.getUserName()));

    // Test this with Google
    GoogleAccessTokenContext grc = createGoogleAccessToken("token1", "rf1", "http://someScope");
    socialNetworkService.updateOAuthInfo(getGoogleProvider(), user1.getUserName(), "googleUsername1", grc);

    userProfile1 = orgService.getUserProfileHandler().findUserProfileByName(user1.getUserName());
    userProfile1.setAttribute(getGoogleProvider().getUserNameAttrName(), "googleUsername2");
    orgService.getUserProfileHandler().saveUserProfile(userProfile1, true);
    assertNull(socialNetworkService.getOAuthAccessToken(getGoogleProvider(), user1.getUserName()));

    orgService.getUserHandler().removeUser(user1.getUserName(), false);
  }

  public void testLongAccessToken() throws Exception {
    // Create some example token of length 800
    User user1 = new UserImpl("testUser1");
    orgService.getUserHandler().createUser(user1, false);

    String longAccessToken = createLongString();

    // FACEBOOK
    // Update long accessToken and verify that it's available
    socialNetworkService.updateOAuthInfo(getFacebookProvider(),
                                         user1.getUserName(),
                                         "fbUsername1",
                                         createFacebookAccessToken(longAccessToken));
    assertEquals(longAccessToken,
                 socialNetworkService.getOAuthAccessToken(getFacebookProvider(), user1.getUserName()).getAccessToken());

    // Update with some short token now
    String shortAccessToken = "someToken1";
    socialNetworkService.updateOAuthInfo(getFacebookProvider(),
                                         user1.getUserName(),
                                         "fbUsername1",
                                         createFacebookAccessToken(shortAccessToken));
    assertEquals(shortAccessToken,
                 socialNetworkService.getOAuthAccessToken(getFacebookProvider(), user1.getUserName()).getAccessToken());

    // GOOGLE
    GoogleAccessTokenContext grc = createGoogleAccessToken(longAccessToken, longAccessToken, "http://someScope");
    socialNetworkService.updateOAuthInfo(getGoogleProvider(), user1.getUserName(), "googleUsername1", grc);
    assertEquals(grc, socialNetworkService.getOAuthAccessToken(getGoogleProvider(), user1.getUserName()));

    socialNetworkService.removeOAuthAccessToken(getGoogleProvider(), user1.getUserName());
    assertNull(socialNetworkService.getOAuthAccessToken(getGoogleProvider(), user1.getUserName()));

    // TWITTER
    TwitterAccessTokenContext twitterToken = new TwitterAccessTokenContext(longAccessToken, longAccessToken);
    socialNetworkService.updateOAuthAccessToken(getTwitterProvider(), user1.getUserName(), twitterToken);
    assertEquals(twitterToken, socialNetworkService.getOAuthAccessToken(getTwitterProvider(), user1.getUserName()));

    twitterToken = new TwitterAccessTokenContext(shortAccessToken, shortAccessToken);
    socialNetworkService.updateOAuthAccessToken(getTwitterProvider(), user1.getUserName(), twitterToken);
    assertEquals(twitterToken, socialNetworkService.getOAuthAccessToken(getTwitterProvider(), user1.getUserName()));

    orgService.getUserHandler().removeUser(user1.getUserName(), false);
  }

  public void testAccessTokensWithMoreScopes() throws Exception {
    User user1 = new UserImpl("testUser1");
    orgService.getUserHandler().createUser(user1, false);

    // Update some google accessToken with two scopes
    GoogleAccessTokenContext googleToken = createGoogleAccessToken("ccc789", "rfrc487", "http://someScope", "http://someScope2");
    socialNetworkService.updateOAuthAccessToken(getGoogleProvider(), user1.getUserName(), googleToken);

    // Verify that Google accessTokens could be obtained and are equals to saved
    // access token
    assertEquals(googleToken, socialNetworkService.getOAuthAccessToken(getGoogleProvider(), user1.getUserName()));

    // Update some google accessToken with two scopes
    FacebookAccessTokenContext facebookToken = createFacebookAccessToken("ddd789", "rfrc4876", "email", "publish_stream");
    socialNetworkService.updateOAuthAccessToken(getFacebookProvider(), user1.getUserName(), facebookToken);

    // Verify that Facebook accessTokens could be obtained and are equals to
    // saved access token
    assertEquals(facebookToken, socialNetworkService.getOAuthAccessToken(getFacebookProvider(), user1.getUserName()));

    orgService.getUserHandler().removeUser(user1.getUserName(), false);
  }

  private OAuthProviderType<FacebookAccessTokenContext> getFacebookProvider() {
    OAuthProviderType<FacebookAccessTokenContext> oAuthProvider = oAuthProviderTypeRegistry.getOAuthProvider(OAuthConstants.OAUTH_PROVIDER_KEY_FACEBOOK,
                                                      FacebookAccessTokenContext.class);
    assertNotNull("Facebook provider is null", oAuthProvider);
    return oAuthProvider;
  }

  private OAuthProviderType<GoogleAccessTokenContext> getGoogleProvider() {
    OAuthProviderType<GoogleAccessTokenContext> oAuthProvider = oAuthProviderTypeRegistry.getOAuthProvider(OAuthConstants.OAUTH_PROVIDER_KEY_GOOGLE, GoogleAccessTokenContext.class);
    assertNotNull("Google provider is null", oAuthProvider);
    return oAuthProvider;
  }

  private OAuthProviderType<TwitterAccessTokenContext> getTwitterProvider() {
    OAuthProviderType<TwitterAccessTokenContext> oAuthProvider = oAuthProviderTypeRegistry.getOAuthProvider(OAuthConstants.OAUTH_PROVIDER_KEY_TWITTER, TwitterAccessTokenContext.class);
    assertNotNull("Twitter provider is null", oAuthProvider);
    return oAuthProvider;
  }

  private GoogleAccessTokenContext createGoogleAccessToken(String accessToken, String refreshToken, String... scope) {
    GoogleTokenResponse grc = new GoogleTokenResponse();
    grc.setAccessToken(accessToken);
    grc.setRefreshToken(refreshToken);
    grc.setExpiresInSeconds(1000L);
    grc.setTokenType("Bearer");
    grc.setIdToken("someTokenId");
    return new GoogleAccessTokenContext(grc, scope);
  }

  private FacebookAccessTokenContext createFacebookAccessToken(String accessToken, String... scope) {
    if (scope == null) {
      scope = new String[] {
          "email"
      };
    }
    return new FacebookAccessTokenContext(accessToken, scope);
  }

  /**
   * Get string of length 800
   * 
   * @return
   */
  private String createLongString() {
    StringBuilder builder =
                          new StringBuilder("1324567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890");
    for (int i = 0; i < 3; i++) {
      builder.append(builder);
    }
    return builder.toString();
  }

}
