package it.com.atlassian.stash;

import com.atlassian.bitbucket.Product;
import com.atlassian.pageobjects.elements.query.Poller;
import com.atlassian.bitbucket.test.DefaultFuncTestData;
import com.atlassian.bitbucket.test.PermissionsTestHelper;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.webdriver.bitbucket.element.BitbucketHeader;
import com.atlassian.webdriver.bitbucket.page.*;
import com.atlassian.webdriver.bitbucket.page.admin.AuthenticationPage;
import com.atlassian.webdriver.bitbucket.page.admin.UserEditPage;
import com.atlassian.webdriver.bitbucket.page.admin.UserListPage;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import org.hamcrest.Matchers;
import org.junit.*;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.WebDriver;
import org.springframework.core.io.ClassPathResource;

import java.util.Arrays;
import java.util.Set;

import static com.atlassian.bitbucket.test.DefaultFuncTestData.getAdminPassword;
import static com.atlassian.bitbucket.test.DefaultFuncTestData.getAdminUser;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

public class LoginTest extends BaseBrowserFuncTest {

    private static String captchaAttemptsSetting;

    @BeforeClass
    public static void setCaptchaToTwo() {
        // Just to be sure - otherwise login may fail below
        BITBUCKET.getTester().getDriver().manage().deleteAllCookies();
        getClassTestContext().user("captchauser", "Capt. Cha", "right", "captcha");
        AuthenticationPage authPage = BITBUCKET.visit(BitbucketLoginPage.class).loginAsSysAdmin(AuthenticationPage.class);
        captchaAttemptsSetting = authPage.getMaxCaptchaAttempts();
        authPage.setMaxCaptchaAttempts("2").clickSave().logout();
    }

    @AfterClass
    public static void restoreCaptchaSetting() {
        BITBUCKET.visit(BitbucketLoginPage.class).loginAsSysAdmin(AuthenticationPage.class).setMaxCaptchaAttempts(captchaAttemptsSetting).clickSave();
    }

    @After
    public void resetRegularUserCaptcha() {
        // Regular user was being captcha'd when it shouldn't have been - resetting
        BITBUCKET.getTester().getDriver().manage().deleteAllCookies();
        UserListPage userListPage = BITBUCKET.visit(BitbucketLoginPage.class).loginAsSysAdmin(UserListPage.class);
        UserEditPage userEditPage = userListPage.getUser(DefaultFuncTestData.getRegularUser()).clickView();

        try {
            if (userEditPage.canClearCaptcha()) {
                userEditPage.clickClearCaptcha().confirmClearCaptcha();
            }
        } finally {
            userEditPage.logout();
        }
    }

    @Test
    public void testLoginAndLogout() {
        final ProjectListPage page = BITBUCKET.visit(BitbucketLoginPage.class).login(DefaultFuncTestData.getRegularUser(), DefaultFuncTestData.getRegularUserPassword(), ProjectListPage.class);
        assertTrue("Should display user in user dropdown", page.getHeader().isLoggedIn());
        final LogoutPage logoutPage = page.getHeader().getUserDropdown().clickTrigger().clickLogout();
        assertFalse("User drop down should not display username when logged out", logoutPage.getHeader().isLoggedIn());
    }

    @Test
    public void testInvalidPassword() throws Exception {
        final BitbucketLoginPage page = BITBUCKET.visit(BitbucketLoginPage.class).loginFail(DefaultFuncTestData.getRegularUser(), "x", false);
        assertFalse("User drop down should not display username when logged out", page.getHeader().isLoggedIn());
        assertThat(page.getAuiErrorMessages(), hasItem("Invalid username or password."));
    }

    @Test
    public void testLoginRememberMeAndLogout() {
        final ProjectListPage page = BITBUCKET.visit(BitbucketLoginPage.class).login(DefaultFuncTestData.getRegularUser(), DefaultFuncTestData.getRegularUserPassword(), true, ProjectListPage.class);
        assertTrue("Should display user in user dropdown", page.getHeader().isLoggedIn());
        final LogoutPage logoutPage = page.getHeader().getUserDropdown().clickTrigger().clickLogout();
        assertFalse("User drop down should not display username when logged out", logoutPage.getHeader().isLoggedIn());
    }

    @Test
    public void testInvalidateOneTimeSession() throws Exception {
        ProjectListPage page = BITBUCKET.visit(BitbucketLoginPage.class).login(DefaultFuncTestData.getRegularUser(), DefaultFuncTestData.getRegularUserPassword(), ProjectListPage.class);
        assertTrue("Should display user in user dropdown", page.getHeader().isLoggedIn());

        invalidateSession();
        BitbucketLoginPage loginPage = BITBUCKET.visit(BitbucketLoginPage.class);
        assertFalse("User drop down should not display username when logged out", loginPage.getHeader().isLoggedIn());
    }

    @Test
    public void testInvalidateRememberMeSession() throws Exception {
        ProjectListPage page = BITBUCKET.visit(BitbucketLoginPage.class).login(DefaultFuncTestData.getRegularUser(), DefaultFuncTestData.getRegularUserPassword(), true, ProjectListPage.class);
        assertTrue("Should display user in user dropdown", page.getHeader().isLoggedIn());

        invalidateSession();
        page = BITBUCKET.visit(ProjectListPage.class);
        assertTrue("Should display user in user dropdown", page.getHeader().isLoggedIn());

        final LogoutPage logoutPage = page.getHeader().getUserDropdown().clickTrigger().clickLogout();
        assertFalse("User drop down should not display username when logged out", logoutPage.getHeader().isLoggedIn());
    }

    @Test
    public void testRedirectNoMVC() throws Exception {
        // For some reason, using ProjectCreatePage.class means that the final URL doesn't contain /mvc.
        // But it "fails" as expected when testing in the browser. Weird.
        String createPageUrl = new ProjectCreatePage().getUrl();
        BITBUCKET.visit(LoginRedirectPage.class, createPageUrl)
                .login(getAdminUser(), getAdminPassword(), BitbucketHomePage.class);
        String currentUrl = BITBUCKET.getTester().getDriver().getCurrentUrl();
        // ?create is not tested by BitbucketHomePage because it's just a query param
        assertThat(currentUrl, containsString(createPageUrl));
        assertThat(currentUrl, Matchers.not(containsString("/mvc/")));
    }

    @Test
    public void testLoginURLShouldContainRequestURLAsQueryParam() throws Exception {
        String createPageUrl = new ProjectCreatePage().getUrl();
        LoginRedirectPage loginPage = BITBUCKET.visit(LoginRedirectPage.class, createPageUrl);
        String currentUrl = BITBUCKET.getTester().getDriver().getCurrentUrl();
        assertThat("Login url should contain next as a query parameter key", currentUrl, containsString("?next="));
        assertThat("Login url should contain ProjectCreatePage url as the query parameter", currentUrl, Matchers.endsWith(createPageUrl));
        loginPage.login(getAdminUser(), getAdminPassword(), BitbucketHomePage.class);
        currentUrl = BITBUCKET.getTester().getDriver().getCurrentUrl();
        assertThat(currentUrl, containsString(createPageUrl));
    }

    @Test
    public void testLoginShouldRedirectToNextParamEvenIfTheNextUrlContainsURLFragment() throws Exception {
        final String TEST_FILE = DefaultFuncTestData.getProject1Repository1Directory1() +
                '/' + DefaultFuncTestData.getProject1Repository1File1();
        String PROJECT_1 = DefaultFuncTestData.getProject1();
        String REPO_1 = DefaultFuncTestData.getProject1Repository1();
        String fileBrowserUrl = new FileBrowserPage(PROJECT_1, REPO_1, TEST_FILE, null, 1).getUrl();
        LoginRedirectPage loginPage = BITBUCKET.visit(LoginRedirectPage.class, fileBrowserUrl);
        String currentUrl = BITBUCKET.getTester().getDriver().getCurrentUrl();
        assertThat("Login url should contain next as a query parameter key", currentUrl, containsString("?next="));
        assertThat("Login url should contain ProjectCreatePage url as the query parameter", currentUrl, Matchers.endsWith(fileBrowserUrl));
        loginPage.login(getAdminUser(), getAdminPassword(), FileBrowserPage.class,
                        PROJECT_1, REPO_1, TEST_FILE, null, 1);
        currentUrl = BITBUCKET.getTester().getDriver().getCurrentUrl();
        assertThat(currentUrl, Matchers.endsWith(fileBrowserUrl));
    }

    @Test
    public void testRedirectionShouldHappenEvenIfCookiesGetDeleted() throws Exception {
        String createPageUrl = new ProjectCreatePage().getUrl();
        LoginRedirectPage loginPage = BITBUCKET.visit(LoginRedirectPage.class, createPageUrl);
        BITBUCKET.getTester().getDriver().manage().deleteAllCookies();
        loginPage.login(getAdminUser(), getAdminPassword(), BitbucketHomePage.class);
        String currentUrl = BITBUCKET.getTester().getDriver().getCurrentUrl();
        assertThat(currentUrl, containsString(createPageUrl));
    }

    @Test
    public void testConsecutiveFailedLoginsShowsCaptcha() throws Exception {
        BitbucketLoginPage login =
                BITBUCKET.visit(BitbucketLoginPage.class)
                        .loginFail("captchauser", "wrong", true)
                        .loginFail("captchauser", "wrong", true);

        assertNotNull(login.getCaptchImageSrc());
    }

    @Test
    public void testCaptchaCanBeRefreshed() throws Exception {
        BitbucketLoginPage login =
                BITBUCKET.visit(BitbucketLoginPage.class)
                        .loginFail("captchauser", "wrong", true)
                        .loginFail("captchauser", "wrong", true);

        Set<String> captchUrls = Sets.newHashSet();
        captchUrls.add(login.getCaptchImageSrc());
        Thread.sleep(1);
        login.clickCaptchaReload();
        captchUrls.add(login.getCaptchImageSrc());
        Thread.sleep(1);
        login.clickCaptchaReload();
        captchUrls.add(login.getCaptchImageSrc());

        assertEquals(3, captchUrls.size());
    }

    @Test
    public void testCaptchaPromptedIfPageStateStale() throws Exception {
        BITBUCKET.visit(BitbucketLoginPage.class)
                .loginFail("captchauser", "wrong", true)
                .loginFail("captchauser", "wrong", true);

        BitbucketLoginPage login =
                BITBUCKET.visit(BitbucketLoginPage.class)
                        .loginFail("captchauser", "wrong", true);

        assertNotNull(login.getCaptchImageSrc());
    }

    @Test
    public void testCaptchaPromptedAfterConsecutiveFailedLoginsEvenIfCorrectPasswordSupplied() throws Exception {
        BitbucketLoginPage login = BITBUCKET.visit(BitbucketLoginPage.class)
                .loginFail("captchauser", "wrong", true)
                .loginFail("captchauser", "wrong", true)
                .loginFail("captchauser", "right", true);

        assertNotNull(login.getCaptchImageSrc());
    }

    @Test
    public void testCanLogInAsUserXAfterConsecutiveFailedLoginsByUserY() throws Exception {
        BITBUCKET.visit(BitbucketLoginPage.class)
                .loginFail("captchauser", "wrong", true)
                .loginFail("captchauser", "wrong", true)
                .login(getAdminUser(), getAdminPassword(), true, ProjectListPage.class)
                .logout();
    }


    @Test
    public void testSecurityCheckUrlShouldReAuthenticateAndOnSuccessShouldForwardToProjectsPage() throws Exception {
        BitbucketHomePage homePage = loginAsAdmin(BitbucketHomePage.class);
        // go to security check
        BITBUCKET.visit(RedirectPage.class, "/j_atl_security_check?j_username=admin&j_password=admin");
        // should land back on the home page
        Poller.waitUntilTrue("Should land back on home page after successful security check", homePage.isHereTimed());
    }

    @Test
    public void testSecurityCheckUrlShouldReAuthenticateAndOnFailureShouldNotForwardToProjectsPage() throws Exception {
        BitbucketLoginPage loginPage = BITBUCKET.visit(BitbucketLoginPage.class);
        // login to home page
        loginPage.login(getAdminUser(), getAdminPassword(), BitbucketHomePage.class);
        // go to security check
        BITBUCKET.visit(RedirectPage.class, "/j_atl_security_check?j_username=admin&j_password=wrongPassword");
        // should land back on login page
        Poller.waitUntilTrue("Should land on login page after unsuccessful security check", loginPage.isHereTimed());
    }

    public void invalidateSession() {
        WebDriver.Options webDriverOptions = BITBUCKET.getTester().getDriver().getDriver().manage();
        for (String cookieName : Arrays.asList("hazelcast.sessionId", "JSESSIONID")) {
            Cookie session = webDriverOptions.getCookieNamed(cookieName);
            if (session != null) {
                webDriverOptions.deleteCookie(session);
            }
        }
    }

    @Test
    public void testLoginWithDifferentPermissions() throws Exception {
        // create a normal user with no default group and no permissions
        String user = "simpleUser";
        String password = "password";
        testContext.user(user, user, password, "simpleuser@example.com", false);

        BitbucketLoginPage loginPage = BITBUCKET.visit(BitbucketLoginPage.class).loginFail(user, password, false);
        assertFalse("User drop down should not display username when logged out", loginPage.getHeader().isLoggedIn());
        assertThat(loginPage.getAuiErrorMessages(), hasItem(String.format("You do not have permission to access %s.", Product.NAME)));

        grantPermissionAndLogIn(user, password, Permission.SYS_ADMIN);
        grantPermissionAndLogIn(user, password, Permission.ADMIN);
        grantPermissionAndLogIn(user, password, Permission.PROJECT_CREATE);
        grantPermissionAndLogIn(user, password, Permission.LICENSED_USER);
    }

    @Test
    public void testLoginAnonymousUserShouldRedirectBackToInitialProjectPage() throws Throwable {
        testContext.project("temp_project");
        testContext.repository("temp_project", "empty_repo", true);
        BitbucketHeader bitbucketHeader = anonymous(ProjectOverviewPage.class, "temp_project").getHeader();
        BitbucketLoginPage stashLoginPage = bitbucketHeader.login();
        ProjectOverviewPage projectOverviewPage = stashLoginPage.
                login(DefaultFuncTestData.getRegularUser(), DefaultFuncTestData.getRegularUserPassword(), ProjectOverviewPage.class, "temp_project");
        assertTrue(projectOverviewPage.isHere());
    }

    @Test
    public void testLoginAnonymousUserShouldRedirectBackToInitialFileBrowserPage() throws Throwable {
        testContext.project("ANONYMOUS", "anonymous_project");
        testContext.repository("ANONYMOUS", "anonymous_repo", new ClassPathResource("git/default-repository.zip"), true);
        FileBrowserPage fileBrowserPage = anonymous(FileBrowserPage.class, "ANONYMOUS", "anonymous_repo");
        fileBrowserPage = fileBrowserPage.clickFolder("binary");
        BitbucketHeader bitbucketHeader = fileBrowserPage.getHeader();
        BitbucketLoginPage stashLoginPage = bitbucketHeader.login();
        FileBrowserPage fileBrowserPageLoggedIn = stashLoginPage.
                login(DefaultFuncTestData.getRegularUser(), DefaultFuncTestData.getRegularUserPassword(), FileBrowserPage.class, "ANONYMOUS", "anonymous_repo", "binary");
        assertTrue(fileBrowserPageLoggedIn.isHere());
    }

    @Test
    public void testLoginEmbeddedForSysadmin() {
        BitbucketLoginPage page = visitLoginWithFeatures(true, Permission.SYS_ADMIN.name());

        assertTrue("Should display embedded form", page.isEmbedded());
        assertEquals(Permission.SYS_ADMIN.name(), page.getPermission());
        assertThat(page.getAuiWarningMessages(), contains(containsString("System Administrator")));

        page = page.loginFail("blah", "bleh", false);

        // make sure all parameters have been carried over
        assertTrue("Should display embedded form after failed login", page.isEmbedded());
        assertEquals(Permission.SYS_ADMIN.name(), page.getPermission());
        assertThat(page.getAuiWarningMessages(), contains(containsString("System Administrator")));
    }

    @Test
    public void testLoginForAdmin() {
        BitbucketLoginPage page = visitLoginWithFeatures(false, Permission.ADMIN.name());

        assertFalse("Should not display embedded form", page.isEmbedded());
        assertEquals(Permission.ADMIN.name(), page.getPermission());
        assertThat(page.getAuiWarningMessages(), contains(containsString("Administrator")));
    }


    private BitbucketLoginPage visitLoginWithFeatures(boolean embedded, String permission) {
        return BITBUCKET.visit(BitbucketLoginPage.class, true, embedded, permission);
    }

    private void grantPermissionAndLogIn(String user, String password, Permission permission) {
        try {
            PermissionsTestHelper.setGlobalPermissionForUser(user, permission.name());
            ProjectListPage page = loginAs(user, password, ProjectListPage.class);
            assertTrue(permission.name() + " should be able to log in", page.getHeader().isLoggedIn());
        } finally {
            PermissionsTestHelper.revokeGlobalPermissionsForUser(user);
        }
    }

    /**
     * A login page that Stash redirects users to if they're not authenticated and requesting a protected page. This
     * page is just a standard {@link BitbucketLoginPage}, except the URL is expected to contain a "next" parameter
     * pointing at the originally requested page.
     */
    public static class LoginRedirectPage extends BitbucketLoginPage {

        private final String redirectUrl;

        public LoginRedirectPage(String url) {
            this.redirectUrl = url;
        }

        @Override
        public boolean isHere() {
            String url = driver.getCurrentUrl();

            return stringContainsInOrder(ImmutableList.of(super.getUrl(), "?next=", redirectUrl)).matches(url)
                    && url.endsWith(redirectUrl);
        }

        @Override
        public String getUrl() {
            return redirectUrl;
        }
    }

    /**
     * A "placeholder" page used to navigate to an arbitrary URL without validating where the browser has landed.
     */
    public static class RedirectPage extends BitbucketPage {

        private final String url;

        public RedirectPage(String url) {
            this.url = url;
        }

        @Override
        public boolean isHere() {
            // no validation
            return true;
        }

        @Override
        public String getUrl() {
            return url;
        }
    }
}
