/*******************************************************************************
 * Copyright (c) 2013, 2014 Lectorius, Inc.
 * Authors:
 * Vijay Pandurangan ([email protected])
 * Evan Jones ([email protected])
 * Adam Hilss ([email protected])
 *
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU 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 General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *     
 *     You can contact the authors at [email protected]
 *******************************************************************************/
package co.mitro.core.servlets;

import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;

import org.junit.Before;
import org.junit.Test;

import co.mitro.core.accesscontrol.AuthenticatedDB;
import co.mitro.core.exceptions.MitroServletException;
import co.mitro.core.exceptions.UserVisibleException;
import co.mitro.core.server.data.DBAcl;
import co.mitro.core.server.data.DBAcl.CyclicGroupError;
import co.mitro.core.server.data.DBGroup;
import co.mitro.core.server.data.DBGroupSecret;
import co.mitro.core.server.data.DBIdentity;
import co.mitro.core.server.data.DBServerVisibleSecret;
import co.mitro.core.server.data.RPC.CreateOrganizationRequest.PrivateGroupKeys;
import co.mitro.core.server.data.RPC.MutateOrganizationRequest;
import co.mitro.core.server.data.RPC.MutateOrganizationResponse;
import co.mitro.core.servlets.MitroServlet.MitroRequestContext;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.j256.ormlite.stmt.DeleteBuilder;

public class MutateOrganizationTest extends OrganizationsFixture {
  
  protected MutateOrganizationResponse resp;
  protected MutateOrganizationRequest rqst;
  protected MutateOrganization mutate;
  
  // NB: The @Before methods of superclasses will be run before those of the current class. 
  // No other ordering is defined.
  @Before 
  public void mutateSetup() {
    rqst = new MutateOrganizationRequest();
    rqst.promotedMemberEncryptedKeys = Maps.newHashMap();
    rqst.orgId = org.getId();
    rqst.newMemberGroupKeys = Maps.newHashMap();
    mutate = new MutateOrganization(managerFactory, keyFactory);
  }

  private void expectException(DBIdentity identity, String substr) throws IOException, SQLException {
    try {
      getMutateResponse(identity);
      fail("expected exception");
    } catch (MitroServletException expected) {
      if (null != substr) {
        assertThat(expected.getMessage().toLowerCase(), containsString(substr.toLowerCase()));
      }
    }
  }
  
  private void getMutateResponse(DBIdentity identity) throws MitroServletException, IOException, SQLException {
    resp = (MutateOrganizationResponse) mutate.processCommand(
        new MitroRequestContext(identity, gson.toJson(rqst), manager, null));
  }

  @Test
  public void testAdminRemovalFailures() throws SQLException, MitroServletException, IOException {

    // bad admin removal requests
    // try to remove a non-existent user 
    rqst.adminsToDemote = ImmutableList.of("[email protected]");
    expectException(testIdentity, "are not admins and could not be deleted");
    
    // try to remove a non-existent user and a real one
    rqst.adminsToDemote = ImmutableList.of("[email protected]", testIdentity.getName());
    expectException(testIdentity, "are not admins and could not be deleted");

    // try to remove a member who isn't an admin
    rqst.adminsToDemote = ImmutableList.of(testIdentity2.getName());
    expectException(testIdentity, "are not admins and could not be deleted");

    // try to remove a member who isn't an admin
    rqst.adminsToDemote = ImmutableList.of("unkonwn", testIdentity2.getName());
    expectException(testIdentity, "are not admins and could not be deleted");
  }
  
  @Test
  public void testMemberRemovalFailures() throws SQLException, MitroServletException, IOException {    
    // bad member removal requests
    // try to remove a non-existent user 
    rqst.membersToRemove = ImmutableList.of("unknown_user[email protected]");
    expectException(testIdentity, "Invalid members to remove");
    
    // try to remove a non-existent user and a real one
    rqst.membersToRemove = ImmutableList.of("[email protected]", testIdentity2.getName());
    expectException(testIdentity, "Invalid members to remove");

    // try to remove a member is an admin without removing him as an admin
    rqst.membersToRemove = ImmutableList.of(testIdentity.getName());
    expectException(testIdentity, "Cannot remove members who are admins");

    rqst.membersToRemove = ImmutableList.of(testIdentity.getName(), testIdentity2.getName());
    expectException(testIdentity, "Cannot remove members who are admins");

    rqst.membersToRemove = ImmutableList.of("unknown", testIdentity.getName());
    expectException(testIdentity, "Invalid members to remove");
  }

  @Test
  public void removeAndPromoteMember() throws IOException, SQLException {
    // remove a member and promote them at the same time
    DBIdentity member = members.iterator().next();
    rqst.membersToRemove = ImmutableList.of(member.getName());
    rqst.promotedMemberEncryptedKeys.put(member.getName(), "org key encrypted for identity");
    expectException(testIdentity, "Cannot add admins without them being members");
  }

  @Test
  public void testUnprivilegedRemovalFailures() throws SQLException, MitroServletException, IOException {    
    // bad member removal requests
    // try to remove a non-existent user 
    rqst.membersToRemove = ImmutableList.of(testIdentity2.getName());
    expectException(testIdentity2, "no access");
    
    rqst.membersToRemove = ImmutableList.of("moo");
    expectException(testIdentity2, "no access");
  }
  
  @Test
  public void testRemoveAllAdminsFailure() throws SQLException, MitroServletException, IOException {
    rqst.adminsToDemote = Lists.newArrayList();
    for (DBIdentity a : admins) {
      rqst.adminsToDemote.add(a.getName());
    }

    // must be user visible
    try {
      getMutateResponse(testIdentity);
      fail("expected exception");
    } catch (UserVisibleException expected) {
      assertThat(expected.getUserVisibleMessage(), containsString("all admins"));
    }
  }
  
  @Test
  public void testAddAdminButNotUserFailure() throws SQLException, MitroServletException, IOException {
    rqst.promotedMemberEncryptedKeys.put(((DBIdentity)outsiders.toArray()[0]).getName(), "org key for admin");
    expectException(testIdentity, "cannot add admins without");
  }

  @Test
  public void testDemoteAdmins() throws SQLException, MitroServletException, IOException {
    rqst.adminsToDemote = ImmutableList.of(testIdentity.getName());
    getMutateResponse(testIdentity);
    admins.remove(testIdentity);
    
    // confusingly admins and members are exclusive -- they have the same meaning as the JS code.
    members.add(testIdentity);
    assertTrue(members.contains(testIdentity));
    checkAll();
  }
  
  @Test
  public void testPromoteUsers() throws SQLException, MitroServletException, IOException {
    DBIdentity member = members.iterator().next();
    rqst.promotedMemberEncryptedKeys.put(member.getName(), "org key for admin");
    getMutateResponse(testIdentity);
    admins.add(member);
    checkAll();
  }
  @Test
  public void testAddMember() throws SQLException, MitroServletException, IOException {
    DBIdentity newMember = outsiders.iterator().next();
    addNewMemberToRequest(newMember.getName());
    getMutateResponse(testIdentity);
    members.add(newMember);
    checkAll();
  }

  private void addNewMemberToRequest(String emailAddress) {
    PrivateGroupKeys memberKeys = new PrivateGroupKeys();
    memberKeys.publicKey = "pub key";
    memberKeys.keyEncryptedForUser = "userkey";
    memberKeys.keyEncryptedForOrganization = "orgkey";
    rqst.newMemberGroupKeys.put(emailAddress, memberKeys);
  }

  @Test
  public void testReAddMemberFailure() throws SQLException, MitroServletException, IOException {
    DBIdentity existingMember = members.iterator().next();
    addNewMemberToRequest(existingMember.getName());
    expectException(testIdentity, "duplicate members");
  }

  @Test
  public void testReAddAdminFailure() throws SQLException, MitroServletException, IOException {
    DBIdentity existingAdmin = admins.iterator().next();
    rqst.promotedMemberEncryptedKeys.put(existingAdmin.getName(), "org key for admin");
    expectException(testIdentity, "duplicate admins");
  }

  @Test
  public void testAddMemberAndAdmin() throws SQLException, MitroServletException, IOException {
    DBIdentity newMemberAndAdmin = outsiders.iterator().next();
    addNewMemberToRequest(newMemberAndAdmin.getName());
    rqst.promotedMemberEncryptedKeys.put(newMemberAndAdmin.getName(), "org key for admin");

    getMutateResponse(testIdentity);
    admins.add(newMemberAndAdmin);
    members.add(newMemberAndAdmin);
    checkAll();
  }

  @Test
  public void testMemberWithOtherGroupRemoval() throws SQLException, MitroServletException, IOException, CyclicGroupError {
    DBGroup namedOrgGroup = createGroupContainingIdentity(testIdentity);
    DBAcl acl = addToGroup(testIdentity2, namedOrgGroup, DBAcl.AccessLevelType.ADMIN);
    addOrgToGroup(manager, org, namedOrgGroup, DBAcl.AccessLevelType.ADMIN);
    rqst.adminsToDemote = Lists.newArrayList();
    rqst.membersToRemove = Lists.newArrayList();
    rqst.adminsToDemote.add(testIdentity.getName());
    rqst.membersToRemove.add(testIdentity.getName());
    getMutateResponse(testIdentity);
    
    // the named group must not have been deleted
    boolean found = false;
    for (DBGroup g : org.getAllOrgGroups(manager)) {
      if (g.getId() == namedOrgGroup.getId()) {
        found = true;
        break;
      }
    }
    assertTrue(found);
    
    // the user must not have access to the group
    namedOrgGroup = manager.groupDao.queryForId(namedOrgGroup.getId());
    for (DBAcl a : namedOrgGroup.getAcls()) {
      if (a.getMemberIdentityIdAsInteger() != null) {
        assertFalse(a.getMemberIdentityIdAsInteger().intValue() == testIdentity.getId());
      }
    }
    admins.remove(testIdentity);
    members.remove(testIdentity);
    checkAll();
  }

  @Test
  public void testMemberRemovePrivateGroup() throws SQLException, MitroServletException, IOException, CyclicGroupError {
    DBIdentity member = members.iterator().next();

    // find this member's private group
    DBGroup privateMemberGroup = getPrivateOrgGroup(org, member);
    assertEquals(2, privateMemberGroup.getAcls().size());
    // add a secret to this member's private group
    DBServerVisibleSecret secret = createSecret(privateMemberGroup, "client", "critical", org);

    // remove member from the organization
    rqst.membersToRemove = Lists.newArrayList(member.getName());
    getMutateResponse(testIdentity);

    // member's private group no longer exists
    assertNull(manager.groupDao.queryForId(privateMemberGroup.getId()));
    // the secret exists but only has 1 group secret (org membership for orphaned secret)
    secret = manager.svsDao.queryForId(secret.getId());
    assertEquals(1, secret.getGroupSecrets().size());
  }

  @Test
  public void emptyOrgMemberRemove() throws SQLException, MitroServletException, IOException, CyclicGroupError {
    // remove all secrets from the organization
    DeleteBuilder<DBGroupSecret, Integer> deleteBuilder = manager.groupSecretDao.deleteBuilder();
    deleteBuilder.where().in(DBGroupSecret.SVS_ID_NAME, orgSecret.getId(), orphanedOrgSecret.getId());
    assertEquals(4, deleteBuilder.delete());

    // remove member from an "empty" organization
    // had a bug where we attempted an IN query on an empty list
    rqst.membersToRemove = new ArrayList<>();
    rqst.membersToRemove.add(members.iterator().next().getName());
    getMutateResponse(testIdentity);
  }

  @SuppressWarnings("deprecation")
  @Test
  public void testMemberRemoveAccess() throws SQLException, MitroServletException, IOException, CyclicGroupError {
    Iterator<DBIdentity> it = members.iterator();
    DBIdentity member1 = it.next();
    DBIdentity member2 = it.next();

    // namedNonOrgGroup contains member and testIdentity2
    DBGroup namedNonOrgGroup = createGroupContainingIdentity(member1);
    addToGroup(testIdentity2, namedNonOrgGroup, DBAcl.AccessLevelType.ADMIN);

    // namedOrgGroup contains member1 and member2
    DBGroup namedOrgGroup = createGroupContainingIdentity(member1);
    addToGroup(member2, namedOrgGroup, DBAcl.AccessLevelType.ADMIN);
    addOrgToGroup(manager, org, namedOrgGroup, DBAcl.AccessLevelType.ADMIN);

    // Add org secret to both groups
    addSecretToGroup(orgSecret, namedOrgGroup, "client", "critical");
    addSecretToGroup(orgSecret, namedNonOrgGroup, "client", "critical");

    // all three have access to the secret
    AuthenticatedDB member1Db = AuthenticatedDB.deprecatedNew(manager, member1);
    AuthenticatedDB member2Db = AuthenticatedDB.deprecatedNew(manager, member2);
    AuthenticatedDB testIdentity2Db = AuthenticatedDB.deprecatedNew(manager, testIdentity2);
    assertEquals(orgSecret.getId(), member1Db.getSecretAsUser(orgSecret.getId()).getId());
    assertEquals(orgSecret.getId(), member2Db.getSecretAsUser(orgSecret.getId()).getId());
    assertEquals(orgSecret.getId(), testIdentity2Db.getSecretAsUser(orgSecret.getId()).getId());

    // remove member1 from the organization
    rqst.adminsToDemote = Lists.newArrayList();
    rqst.membersToRemove = Lists.newArrayList();
    rqst.adminsToDemote = Lists.newArrayList();
    rqst.membersToRemove.add(member1.getName());
    getMutateResponse(testIdentity);

    // member1 no longer has access. member2 has access via namedOrgGroup. testIdentity2 loses access
    assertNull(member1Db.getSecretAsUser(orgSecret.getId()));
    assertEquals(orgSecret.getId(), member2Db.getSecretAsUser(orgSecret.getId()).getId());
    assertNull(testIdentity2Db.getSecretAsUser(orgSecret.getId()));
  }

  private void checkAdmins() throws SQLException {
    Collection<DBAcl> acls = org.getAcls();
    Set<DBIdentity> actualAdmins = Sets.newHashSet();
    assertEquals(admins.size(), acls.size());
    for (DBAcl a : acls) {
      DBIdentity i = a.loadMemberIdentity(manager.identityDao);
      actualAdmins.add(i);
    }
    assertTrue((Sets.symmetricDifference(actualAdmins, admins)).isEmpty());
  }

  private void checkMembers() throws SQLException {
    Set<Integer> actualMemberIds = MutateOrganization.getMemberIdsAndPrivateGroupIdsForOrg(manager, org).keySet();
    Set<Integer> expectedMemberIds = Sets.newHashSet();
    for (DBIdentity i : members) {
      expectedMemberIds.add(i.getId());
    }
    for (DBIdentity i : admins) {
      expectedMemberIds.add(i.getId());
    }
    assertTrue((Sets.symmetricDifference(actualMemberIds, expectedMemberIds)).isEmpty());
  }

  public void checkAll() throws SQLException {
    checkAdmins();
    checkMembers();
  }
}