package org.jooby.internal.pac4j;

import static org.easymock.EasyMock.expect;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import java.util.Arrays;
import java.util.Collections;
import java.util.Optional;

import org.jooby.Err;
import org.jooby.Mutant;
import org.jooby.Request;
import org.jooby.Response;
import org.jooby.Route;
import org.jooby.pac4j.Auth;
import org.jooby.pac4j.AuthStore;
import org.jooby.test.MockUnit;
import org.jooby.test.MockUnit.Block;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.pac4j.core.client.Client;
import org.pac4j.core.client.Clients;
import org.pac4j.core.client.finder.ClientFinder;
import org.pac4j.core.context.WebContext;
import org.pac4j.core.credentials.Credentials;
import org.pac4j.core.exception.HttpAction;
import org.pac4j.core.exception.TechnicalException;
import org.pac4j.core.profile.CommonProfile;
import org.pac4j.core.profile.UserProfile;
import org.pac4j.http.client.direct.ParameterClient;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import javaslang.control.Try;

@RunWith(PowerMockRunner.class)
@PrepareForTest({AuthFilter.class, Clients.class, ParameterClient.class })
public class AuthFilterTest {

  private Block webctx = unit -> {
    Request req = unit.get(Request.class);
    expect(req.require(WebContext.class)).andReturn(unit.get(WebContext.class));
  };

  private Block cfinder = unit -> {
    Request req = unit.get(Request.class);
    expect(req.require(ClientFinder.class)).andReturn(unit.get(ClientFinder.class));
  };

  private Block astore = unit -> {
    Request req = unit.get(Request.class);
    expect(req.require(AuthStore.class)).andReturn(unit.get(AuthStore.class));
  };

  private Block clients = unit -> {
    Clients clients = unit.get(Clients.class);

    Request req = unit.get(Request.class);
    expect(req.require(Clients.class)).andReturn(clients);
  };

  private Block chainNext = unit -> {
    Route.Chain chain = unit.get(Route.Chain.class);
    chain.next(unit.get(Request.class), unit.get(Response.class));
  };

  @SuppressWarnings("rawtypes")
  private <C extends Client> Block findClient(final Class<C> clientType, final String client) {
    return unit -> {
      C c = Try.of(() -> unit.get(clientType))
          .getOrElse(() -> {
            C m = unit.powerMock(clientType);
            //C m = (C) new ParameterClient("token", new SimpleTestUsernamePasswordAuthenticator());
            unit.registerMock(clientType, m);
            return m;
          });

      ClientFinder finder = unit.get(ClientFinder.class);
      expect(finder.find(unit.get(Clients.class), unit.get(WebContext.class), client))
          .andReturn(Arrays.asList(c));
    };
  }

  @SuppressWarnings("rawtypes")
  private <C extends Client> Block findNoClient(final Class<C> clientType, final String client) {
    return unit -> {

      ClientFinder finder = unit.get(ClientFinder.class);
      expect(finder.find(unit.get(Clients.class), unit.get(WebContext.class), client))
          .andReturn(Collections.emptyList());
    };
  }

  private Block reqAuthID(final String id) {
    return unit -> {
      Request req = unit.get(Request.class);
      expect(req.ifGet(Auth.ID)).andReturn(Optional.ofNullable(id));
    };
  }

  @SuppressWarnings("rawtypes")
  private Block authStore(final String id, final CommonProfile profile) {
    return unit -> {
      AuthStore store = unit.get(AuthStore.class);
      expect(store.get(id)).andReturn(Optional.ofNullable(profile));
    };
  }

  @SuppressWarnings("rawtypes")
  private <C extends Client> Block creds(final Class<C> clientType) {
    return unit -> {
      Credentials creds = unit.mock(Credentials.class);
      unit.registerMock(Credentials.class, creds);

      WebContext ctx = unit.get(WebContext.class);
      C client = unit.get(clientType);
      expect(client.getCredentials(ctx)).andReturn(creds);
    };
  }

  @SuppressWarnings("rawtypes")
  private <C extends Client> Block noCreds(final Class<C> clientType) {
    return unit -> {
      WebContext ctx = unit.get(WebContext.class);
      C client = unit.get(clientType);
      expect(client.getCredentials(ctx))
          .andThrow(HttpAction.forbidden("intentional err", ctx));
    };
  }

  @SuppressWarnings({"rawtypes", "unchecked" })
  private <C extends Client> Block userProfile(final Class<C> clientType,
      final CommonProfile profile) {
    return unit -> {
      Credentials creds = unit.get(Credentials.class);

      WebContext ctx = unit.get(WebContext.class);
      C client = unit.get(clientType);
      expect(client.getUserProfile(creds, ctx)).andReturn(profile);
    };
  }

  private Block setProfileId(final String id) {
    return unit -> {
      Request req = unit.get(Request.class);
      expect(req.set(Auth.ID, id)).andReturn(req);
    };
  }

  @SuppressWarnings({"unchecked", "rawtypes" })
  private Block setProfile(final CommonProfile profile) {
    return unit -> {
      AuthStore store = unit.get(AuthStore.class);
      store.set(profile);
    };
  }

  @SuppressWarnings("rawtypes")
  private Block seed(final Class type, final CommonProfile profile) {
    return unit -> {
      Request req = unit.get(Request.class);
      expect(req.set(type, profile)).andReturn(req);
    };
  }

  private Block clientName(final String name) {
    return unit -> {
      Clients clients = unit.get(Clients.class);
      expect(clients.getClientNameParameter()).andReturn("client_name");

      Mutant client_name = unit.mock(Mutant.class);
      expect(client_name.value(name)).andReturn(name);

      Request req = unit.get(Request.class);
      expect(req.param("client_name")).andReturn(client_name);
    };
  }

  @Test
  public void name() throws Exception {
    String profileId = "123";
    CommonProfile profile = new CommonProfile();
    profile.setId(profileId);

    new MockUnit(Request.class, Response.class, Route.Chain.class, WebContext.class,
        ClientFinder.class, AuthStore.class, Clients.class).run(unit -> {
          AuthFilter filter = new AuthFilter(ParameterClient.class, CommonProfile.class);
          assertEquals("ParameterClient", filter.getName());
          filter.setName("Basic");
          assertEquals("ParameterClient,Basic", filter.getName());
        });
  }

  @Test
  public void handleDirect() throws Exception {
    String profileId = "123";
    CommonProfile profile = new CommonProfile();
    profile.setId(profileId);

    new MockUnit(Request.class, Response.class, Route.Chain.class, WebContext.class,
        ClientFinder.class, AuthStore.class, Clients.class)
            .expect(webctx)
            .expect(cfinder)
            .expect(astore)
            .expect(clients)
            .expect(clientName("ParameterClient"))
            .expect(findClient(ParameterClient.class, "ParameterClient"))
            .expect(reqAuthID(profileId))
            .expect(reqAuthCNAME(ParameterClient.class))
            .expect(authStore(profileId, null))
            .expect(creds(ParameterClient.class))
            .expect(userProfile(ParameterClient.class, profile))
            .expect(setProfileId(profileId))
            .expect(setProfile(profile))
            .expect(seed(UserProfile.class, profile))
            .expect(seed(CommonProfile.class, profile))
            .expect(chainNext)
            .run(unit -> {
              new AuthFilter(ParameterClient.class, CommonProfile.class)
                  .handle(unit.get(Request.class), unit.get(Response.class),
                      unit.get(Route.Chain.class));
            });
  }

  @SuppressWarnings("rawtypes")
  private Block reqAuthCNAME(final Class<? extends Client> client) {
    return unit -> {
      Client c = unit.get(client);
      expect(c.getName()).andReturn(client.getSimpleName());

      Request req = unit.get(Request.class);
      expect(req.set(Auth.CNAME, client.getSimpleName())).andReturn(req);
    };
  }

  @Test
  public void handleDirectNoClient() throws Exception {
    String profileId = "123";
    CommonProfile profile = new CommonProfile();
    profile.setId(profileId);

    new MockUnit(Request.class, Response.class, Route.Chain.class, WebContext.class,
        ClientFinder.class, AuthStore.class, Clients.class)
            .expect(webctx)
            .expect(cfinder)
            .expect(astore)
            .expect(clients)
            .expect(clientName("ParameterClient"))
            .expect(findNoClient(ParameterClient.class, "ParameterClient"))
            .expect(findNoClient(ParameterClient.class, "ParameterClient"))
            .run(unit -> {
              try {
                new AuthFilter(ParameterClient.class, CommonProfile.class)
                    .handle(unit.get(Request.class), unit.get(Response.class));
                fail("expecting 401");
              } catch (Err ex) {
                assertEquals(401, ex.statusCode());
              }
            });
  }

  @Test(expected = TechnicalException.class)
  public void handleDirectNoCredentials() throws Exception {
    String profileId = "123";
    CommonProfile profile = new CommonProfile();
    profile.setId(profileId);

    new MockUnit(Request.class, Response.class, Route.Chain.class, WebContext.class,
        ClientFinder.class, AuthStore.class, Clients.class)
            .expect(webctx)
            .expect(cfinder)
            .expect(astore)
            .expect(clients)
            .expect(clientName("ParameterClient"))
            .expect(findClient(ParameterClient.class, "ParameterClient"))
            .expect(reqAuthID(profileId))
            .expect(authStore(profileId, null))
            .expect(noCreds(ParameterClient.class))
            .run(unit -> {
              new AuthFilter(ParameterClient.class, CommonProfile.class)
                  .handle(unit.get(Request.class), unit.get(Response.class),
                      unit.get(Route.Chain.class));
            });
  }

  @Test(expected = Err.class)
  public void handleDirectNoProfile() throws Exception {
    String profileId = "123";
    CommonProfile profile = null;

    new MockUnit(Request.class, Response.class, Route.Chain.class, WebContext.class,
        ClientFinder.class, AuthStore.class, Clients.class)
            .expect(webctx)
            .expect(cfinder)
            .expect(astore)
            .expect(clients)
            .expect(clientName("ParameterClient"))
            .expect(findClient(ParameterClient.class, "ParameterClient"))
            .expect(findClient(ParameterClient.class, "ParameterClient"))
            .expect(reqAuthID(profileId))
            .expect(authStore(profileId, null))
            .expect(creds(ParameterClient.class))
            .expect(userProfile(ParameterClient.class, profile))
            .expect(setProfileId(profileId))
            .expect(setProfile(profile))
            .run(unit -> {
              new AuthFilter(ParameterClient.class, CommonProfile.class)
                  .handle(unit.get(Request.class), unit.get(Response.class),
                      unit.get(Route.Chain.class));
            });
  }

}
