package org.jooby.internal;

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

import java.util.Optional;

import org.jooby.Cookie;
import org.jooby.Mutant;
import org.jooby.Request;
import org.jooby.Response;
import org.jooby.Result;
import org.jooby.Route;
import org.jooby.Route.After;
import org.jooby.Session;
import org.jooby.internal.parser.ParserExecutor;
import org.jooby.test.MockUnit;
import org.jooby.test.MockUnit.Block;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import com.google.common.collect.ImmutableMap;
import com.typesafe.config.Config;

@RunWith(PowerMockRunner.class)
@PrepareForTest({CookieSessionManager.class, SessionImpl.class, Cookie.class })
public class CookieSessionManagerTest {

  private Block cookie = unit -> {
    Session.Definition sdef = unit.get(Session.Definition.class);
    expect(sdef.cookie()).andReturn(unit.get(Cookie.Definition.class));
  };

  private Block push = unit -> {
    Response rsp = unit.get(Response.class);
    rsp.after(unit.capture(Route.After.class));
  };

  @Test
  public void newInstance() throws Exception {
    String secret = "shhh";
    new MockUnit(Session.Definition.class, ParserExecutor.class, Cookie.Definition.class)
        .expect(cookie)
        .expect(maxAge(-1))
        .run(unit -> {
          new CookieSessionManager(unit.get(ParserExecutor.class),
              unit.get(Session.Definition.class), secret);
        });
  }

  @Test
  public void destroy() throws Exception {
    String secret = "shhh";
    new MockUnit(Session.Definition.class, ParserExecutor.class, Cookie.Definition.class,
        Session.class)
            .expect(cookie)
            .expect(maxAge(-1))
            .run(unit -> {
              new CookieSessionManager(unit.get(ParserExecutor.class),
                  unit.get(Session.Definition.class), secret)
                      .destroy(unit.get(Session.class));
            });
  }

  @Test
  public void requestDone() throws Exception {
    String secret = "shhh";
    new MockUnit(Session.Definition.class, ParserExecutor.class, Cookie.Definition.class,
        Session.class)
            .expect(cookie)
            .expect(maxAge(-1))
            .run(unit -> {
              new CookieSessionManager(unit.get(ParserExecutor.class),
                  unit.get(Session.Definition.class), secret)
                      .requestDone(unit.get(Session.class));
            });
  }

  @Test
  public void create() throws Exception {
    String secret = "shhh";
    new MockUnit(Session.Definition.class, ParserExecutor.class, Cookie.Definition.class,
        Request.class, Response.class, SessionImpl.class)
            .expect(cookie)
            .expect(maxAge(-1))
            .expect(sessionBuilder(Session.COOKIE_SESSION, true, -1))
            .expect(push)
            .run(unit -> {
              Session session = new CookieSessionManager(unit.get(ParserExecutor.class),
                  unit.get(Session.Definition.class), secret)
                      .create(unit.get(Request.class), unit.get(Response.class));
              assertEquals(unit.get(SessionImpl.class), session);
            });
  }

  @Test
  public void saveAfter() throws Exception {
    String secret = "shhh";
    String signed = "$#!";
    new MockUnit(Session.Definition.class, ParserExecutor.class, Cookie.Definition.class,
        Request.class, Response.class, SessionImpl.class)
            .expect(cookie)
            .expect(maxAge(-1))
            .expect(sessionBuilder(Session.COOKIE_SESSION, true, -1))
            .expect(push)
            .expect(unit -> {
              Cookie.Definition cookie = unit.get(Cookie.Definition.class);
              expect(cookie.name()).andReturn(Optional.of("sid"));

              Mutant mutant = unit.mock(Mutant.class);
              expect(mutant.toOptional()).andReturn(Optional.of(signed));

              Request req = unit.get(Request.class);
              expect(req.cookie("sid")).andReturn(mutant);
            })
            .expect(unit -> {
              SessionImpl session = unit.get(SessionImpl.class);

              expect(session.attributes()).andReturn(ImmutableMap.of("foo", "2"));

              Request req = unit.get(Request.class);
              expect(req.ifSession()).andReturn(Optional.of(session));
            })
            .expect(unit -> {
              unit.mockStatic(Cookie.Signature.class);
              expect(Cookie.Signature.unsign(signed, secret)).andReturn("foo=1");
            })
            .expect(signCookie(secret, "foo=2", "sss"))
            .expect(sendCookie())
            .run(unit -> {
              Session session = new CookieSessionManager(unit.get(ParserExecutor.class),
                  unit.get(Session.Definition.class), secret)
                      .create(unit.get(Request.class), unit.get(Response.class));
              assertEquals(unit.get(SessionImpl.class), session);
            }, unit -> {
              After next = unit.captured(Route.After.class).iterator().next();
              Result ok = next.handle(unit.get(Request.class), unit.get(Response.class),
                  org.jooby.Results.ok());
              assertNotNull(ok);
            });
  }

  @Test
  public void ignoreSaveAfterIfNoSession() throws Exception {
    String secret = "shhh";
    new MockUnit(Session.Definition.class, ParserExecutor.class, Cookie.Definition.class,
        Request.class, Response.class, SessionImpl.class)
            .expect(cookie)
            .expect(maxAge(-1))
            .expect(sessionBuilder(Session.COOKIE_SESSION, true, -1))
            .expect(push)
            .expect(unit -> {
              Request req = unit.get(Request.class);
              expect(req.ifSession()).andReturn(Optional.empty());
            })
            .run(unit -> {
              Session session = new CookieSessionManager(unit.get(ParserExecutor.class),
                  unit.get(Session.Definition.class), secret)
                      .create(unit.get(Request.class), unit.get(Response.class));
              assertEquals(unit.get(SessionImpl.class), session);
            }, unit -> {
              After next = unit.captured(Route.After.class).iterator().next();
              Result ok = next.handle(unit.get(Request.class), unit.get(Response.class),
                  org.jooby.Results.ok());
              assertNotNull(ok);
            });
  }

  @Test
  public void saveAfterTouchSession() throws Exception {
    String secret = "shhh";
    String signed = "$#!";
    new MockUnit(Session.Definition.class, ParserExecutor.class, Cookie.Definition.class,
        Request.class, Response.class, SessionImpl.class)
            .expect(cookie)
            .expect(maxAge(30))
            .expect(sessionBuilder(Session.COOKIE_SESSION, true, -1))
            .expect(push)
            .expect(unit -> {
              Cookie.Definition cookie = unit.get(Cookie.Definition.class);
              expect(cookie.name()).andReturn(Optional.of("sid"));

              Mutant mutant = unit.mock(Mutant.class);
              expect(mutant.toOptional()).andReturn(Optional.of(signed));

              Request req = unit.get(Request.class);
              expect(req.cookie("sid")).andReturn(mutant);
            })
            .expect(unit -> {
              SessionImpl session = unit.get(SessionImpl.class);

              expect(session.attributes()).andReturn(ImmutableMap.of("foo", "1"));

              Request req = unit.get(Request.class);
              expect(req.ifSession()).andReturn(Optional.of(session));
            })
            .expect(unit -> {
              unit.mockStatic(Cookie.Signature.class);
              expect(Cookie.Signature.unsign(signed, secret)).andReturn("foo=1");
            })
            .expect(unit -> {
              Cookie.Definition cookie = unit.get(Cookie.Definition.class);
              Cookie.Definition newCookie = unit.constructor(Cookie.Definition.class)
                  .build(cookie);

              expect(newCookie.value(signed)).andReturn(newCookie);
              unit.registerMock(Cookie.Definition.class, newCookie);
            })
            .expect(sendCookie())
            .run(unit -> {
              Session session = new CookieSessionManager(unit.get(ParserExecutor.class),
                  unit.get(Session.Definition.class), secret)
                      .create(unit.get(Request.class), unit.get(Response.class));
              assertEquals(unit.get(SessionImpl.class), session);
            }, unit -> {
              After next = unit.captured(Route.After.class).iterator().next();
              Result ok = next.handle(unit.get(Request.class), unit.get(Response.class),
                  org.jooby.Results.ok());
              assertNotNull(ok);
            });
  }

  private Block sendCookie() {
    return unit -> {
      Cookie.Definition cookie = unit.get(Cookie.Definition.class);
      Response rsp = unit.get(Response.class);
      expect(rsp.cookie(cookie)).andReturn(rsp);
    };
  }

  @Test
  public void noSession() throws Exception {
    String secret = "shh";
    new MockUnit(Config.class, Session.Definition.class, Cookie.Definition.class,
        Session.Store.class, ParserExecutor.class, Request.class, Response.class)
            .expect(cookie)
            .expect(maxAge(-1))
            .expect(unit -> {
              Cookie.Definition cookie = unit.get(Cookie.Definition.class);
              expect(cookie.name()).andReturn(Optional.of("sid"));

              Mutant mutant = unit.mock(Mutant.class);
              expect(mutant.toOptional()).andReturn(Optional.empty());

              Request req = unit.get(Request.class);
              expect(req.cookie("sid")).andReturn(mutant);
            })
            .run(unit -> {
              Session session = new CookieSessionManager(unit.get(ParserExecutor.class),
                  unit.get(Session.Definition.class), secret).get(unit.get(Request.class),
                      unit.get(Response.class));
              assertEquals(null, session);
            });
  }

  @Test
  public void getSession() throws Exception {
    String secret = "shh";
    String signed = "$#!";
    new MockUnit(Config.class, Session.Definition.class, Cookie.Definition.class,
        Session.Store.class, ParserExecutor.class, Request.class, Response.class, SessionImpl.class)
            .expect(cookie)
            .expect(maxAge(-1))
            .expect(unit -> {
              Cookie.Definition cookie = unit.get(Cookie.Definition.class);
              expect(cookie.name()).andReturn(Optional.of("sid"));

              Mutant mutant = unit.mock(Mutant.class);
              expect(mutant.toOptional()).andReturn(Optional.of(signed));

              Request req = unit.get(Request.class);
              expect(req.cookie("sid")).andReturn(mutant);
            })
            .expect(unit -> {
              unit.mockStatic(Cookie.Signature.class);
              expect(Cookie.Signature.unsign(signed, secret)).andReturn("foo=1");
            })
            .expect(sessionBuilder(Session.COOKIE_SESSION, false, -1))
            .expect(unit -> {
              Session.Builder builder = unit.get(Session.Builder.class);
              expect(builder.set(ImmutableMap.of("foo", "1"))).andReturn(builder);
              expect(builder.build()).andReturn(unit.get(SessionImpl.class));
            })
            .expect(push)
            .run(unit -> {
              Session session = new CookieSessionManager(unit.get(ParserExecutor.class),
                  unit.get(Session.Definition.class), secret).get(unit.get(Request.class),
                      unit.get(Response.class));
              assertEquals(unit.get(SessionImpl.class), session);
            });
  }

  private Block signCookie(final String secret, final String value, final String signed) {
    return unit -> {
      unit.mockStatic(Cookie.Signature.class);
      expect(Cookie.Signature.sign(value, secret)).andReturn(signed);

      Cookie.Definition cookie = unit.get(Cookie.Definition.class);
      Cookie.Definition newCookie = unit.constructor(Cookie.Definition.class)
          .build(cookie);

      expect(newCookie.value(signed)).andReturn(newCookie);
      unit.registerMock(Cookie.Definition.class, newCookie);
    };
  }

  private Block maxAge(final Integer maxAge) {
    return unit -> {
      Cookie.Definition session = unit.get(Cookie.Definition.class);
      expect(session.maxAge()).andReturn(Optional.of(maxAge));
    };
  }

  private Block sessionBuilder(final String id, final boolean isNew, final long timeout) {
    return unit -> {
      SessionImpl.Builder builder = unit.constructor(SessionImpl.Builder.class)
          .build(unit.get(ParserExecutor.class), isNew, id, timeout);
      if (isNew) {
        expect(builder.build()).andReturn(unit.get(SessionImpl.class));
      }

      unit.registerMock(Session.Builder.class, builder);
    };
  }

}
