package base;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import controllers.Component;
import models.Account;
import models.Friendship;
import models.Group;
import models.GroupAccount;
import models.enums.AccountRole;
import models.enums.GroupType;
import models.enums.LinkType;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import play.db.jpa.JPA;
import play.libs.F;
import play.mvc.Http;
import play.test.FakeApplication;
import play.test.Helpers;

import javax.inject.Inject;
import java.util.Collections;
import java.util.Map;

import static org.mockito.Mockito.*;

/**
 * You can extend your test classes from this class to provide a complete fake environment.
 */
public abstract class FakeApplicationTest {

    @Inject
    GroupAccount groupAccount;

    /**
     * Mocks a HTTP request instance.
     */
    private final Http.Request request = mock(Http.Request.class);

    /**
     * Hols the FakeApplication instance.
     */
    public static FakeApplication app;

    /**
     * The default password for test accounts.
     */
    public static final String TEST_ACCOUNT_PASSWORD = "<PASSWORD>";

    /**
     * Holds the singleton Config instance for the current context.
     */
    private static Config configuration = null;

    /**
     * Returns the configuration value for a configuration key.
     *
     * @param configurationKey Configuration key
     * @return Configuration value
     */
    protected static String getConfiguration(String configurationKey) {
        if (FakeApplicationTest.configuration == null) {
            FakeApplicationTest.configuration = ConfigFactory.load();
        }

        return FakeApplicationTest.configuration.getString(configurationKey);
    }

    @BeforeClass
    public static void startApp() {
        app = Helpers.fakeApplication();
        Helpers.start(app);
    }

    @Before
    public void setUp() throws Exception {
        Map<String, String> flashData = Collections.emptyMap();
        Map<String, Object> argData = Collections.emptyMap();
        Long id = 2L;
        play.api.mvc.RequestHeader header = mock(play.api.mvc.RequestHeader.class);
        Http.Context context = new Http.Context(id, header, this.request, flashData, flashData, argData);
        Http.Context.current.set(context);
    }

    @AfterClass
    public static void stopApp() {
        Helpers.stop(app);
    }

    /**
     * Returns an account by email.
     *
     * @param email E-Mail address of account to fetch
     * @return Account instance
     */
    public Account getAccountByEmail(final String email) {
        try {
            return JPA.withTransaction(new F.Function0<Account>() {
                @Override
                public Account apply() throws Throwable {
                    return Account.findByEmail(email);
                }
            });
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            return null;
        }
    }

    /**
     * Returns a test account, creates one before if not exists.
     *
     * @param number Number of test account
     * @return Account instance
     */
    public Account getTestAccount(final int number) {
        final String testAccountEmail = "test" + String.valueOf(number) + "@socia.de";
        Account storedTestAccount = this.getAccountByEmail(testAccountEmail);

        // if there is this test account, return
        if (storedTestAccount != null) {
            return storedTestAccount;
        }

        // there is no test account with that number right now, create a persistent one
        try {
            return JPA.withTransaction(new F.Function0<Account>() {
                @Override
                public Account apply() throws Throwable {
                    Account testAccount = new Account();
                    testAccount.firstname = "Test";
                    testAccount.lastname = "User " + String.valueOf(number);
                    testAccount.email = testAccountEmail;
                    testAccount.avatar = "a1";
                    testAccount.role = AccountRole.STUDENT;
                    testAccount.password = <PASSWORD>(FakeApplicationTest.TEST_ACCOUNT_PASSWORD);
                    testAccount.create();

                    return testAccount;
                }
            });
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            return null;
        }
    }

    /**
     * Returns the default admin account.
     *
     * @return Account instance
     */
    public Account getAdminAccount() {
        String adminEmailAddress = FakeApplicationTest.getConfiguration("socia.admin.<EMAIL>");
        return this.getAccountByEmail(adminEmailAddress);
    }

    /**
     * Establishes a friendship between test accounts.
     *
     * @param testAccountA First test account
     * @param testAccountB Second test account
     */
    public void establishFriendshipTestAccounts(final Account testAccountA, final Account testAccountB) {
        JPA.withTransaction(new F.Callback0() {
            @Override
            public void invoke() throws Throwable {
                if (!Friendship.alreadyFriendly(testAccountA, testAccountB)) {
                    Friendship testFriendship = new Friendship(testAccountA, testAccountB, LinkType.establish);
                    testFriendship.create();
                }
                if (!Friendship.alreadyFriendly(testAccountB, testAccountA)) {
                    Friendship testFriendship = new Friendship(testAccountB, testAccountA, LinkType.establish);
                    testFriendship.create();
                }
            }
        });
    }

    /**
     * Removes a friendship between test accounts.
     *
     * @param testAccountA First test account
     * @param testAccountB Second test account
     */
    public void removeFriendshipTestAccounts(final Account testAccountA, final Account testAccountB) {
        JPA.withTransaction(new F.Callback0() {
            @Override
            public void invoke() throws Throwable {
                if (Friendship.alreadyFriendly(testAccountA, testAccountB)) {
                    Friendship.findFriendLink(testAccountA, testAccountB).delete();
                }
                if (Friendship.alreadyFriendly(testAccountB, testAccountA)) {
                    Friendship.findFriendLink(testAccountB, testAccountA).delete();
                }
            }
        });
    }


    /**
     * Returns a group by title.
     *
     * @param title Title of group to fetch
     * @return Group instance
     */
    public Group getGroupByTitle(final String title) {
        try {
            return JPA.withTransaction(new F.Function0<Group>() {
                @Override
                public Group apply() throws Throwable {
                    return Group.findByTitle(title);
                }
            });
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            return null;
        }
    }

    /**
     * Returns a test group, creates one before if not exists.
     *
     * @param number Number of test group
     * @param groupOwner the optional group owner
     * @return Group instance
     */
    public Group getTestGroup(final int number, final Account groupOwner) {
        final String testGroupTitle = "Test Group " + String.valueOf(number);
        Group storedTestGroup = this.getGroupByTitle(testGroupTitle);

        // if there is this test group, return
        if (storedTestGroup != null) {
            return storedTestGroup;
        }

        // there is no test account with that number right now, create a persistent one
        try {
            return JPA.withTransaction(new F.Function0<Group>() {
                @Override
                public Group apply() throws Throwable {
                    Group testGroup = new Group();
                    testGroup.groupType = GroupType.close;
                    testGroup.setTitle(testGroupTitle);

                    if (groupOwner != null) {
                        testGroup.createWithGroupAccount(groupOwner);
                    } else {
                        testGroup.create();
                    }

                    return testGroup;
                }
            });
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            return null;
        }
    }

    /**
     * Returns a group account for an account to a group.
     *
     * @param account Account
     * @param group Group
     * @return GroupAccount instance if found, otherwise null
     */
    public GroupAccount getGroupAccount(final Account account, final Group group) {
        try {
            return JPA.withTransaction(new F.Function0<GroupAccount>() {
                @Override
                public GroupAccount apply() throws Throwable {
                    return GroupAccount.find(account, group);
                }
            });
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            return null;
        }
    }

    /**
     * Establishes a group membership of an account to a group.
     *
     * @param account Account
     * @param group Group
     */
    public void establishGroupMembership(final Account account, final Group group) {
        JPA.withTransaction(new F.Callback0() {
            @Override
            public void invoke() throws Throwable {
                if (!Group.isMember(group, account)) {
                    groupAccount.account = account;
                    groupAccount.group = group;
                    groupAccount.linkType = LinkType.establish;
                    testGroupAccount.create();
                    group.update();
                }
            }
        });
    }

    /**
     * Removes a membership of an account to a group.
     *
     * @param account Account
     * @param group Group
     */
    public void removeGroupMembership(final Account account, final Group group) {
        JPA.withTransaction(new F.Callback0() {
            @Override
            public void invoke() throws Throwable {
                if (Group.isMember(group, account)) {
                    GroupAccount groupAccount = GroupAccount.find(account, group);
                    groupAccount.delete();
                }
            }
        });
    }

    /**
     * Logs in an account into the test HTTP context.
     *
     * @param account Account to login
     */
    public void loginAccount(Account account) {
        Http.Context.current().session().put("id", account.id.toString());
        Http.Context.current().args.put("account", account);
    }

    /**
     * Logs in a test account.
     *
     * @param testAccountNumber Test account number
     */
    public void loginTestAccount(int testAccountNumber) {
        Account testAccount = this.getTestAccount(testAccountNumber);
        this.loginAccount(testAccount);
    }

    /**
     * Logs in the default admin account.
     */
    public void loginAdminAccount() {
        Account adminAccount = this.getAdminAccount();
        this.loginAccount(adminAccount);
    }

    /**
     * Logs out a test account.
     */
    public void logoutTestAccount() {
        Http.Context.current().session().remove("id");
    }

    /**
     * Force a thread sleep.
     *
     * @param seconds Seconds how long to sleep
     */
    protected void sleep(int seconds) {
        try {
            Thread.sleep(seconds * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * Returns the current HTTP context.
     *
     * @return Context instance
     */
    protected Http.Context getContext() {
        return Http.Context.current();
    }
}
