package org.jooby.cassandra;

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

import java.util.Date;
import java.util.Map;

import org.jooby.Session.Builder;
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.datastax.driver.core.BoundStatement;
import com.datastax.driver.core.PreparedStatement;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.ResultSetFuture;
import com.datastax.driver.core.Row;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.Statement;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;

@RunWith(PowerMockRunner.class)
@PrepareForTest({CassandraSessionStore.class, Futures.class, BoundStatement.class })
public class CassandraSessionStoreTest {

  @SuppressWarnings("unchecked")
  private Block createTable = unit -> {
    ResultSetFuture rs = unit.mock(ResultSetFuture.class);

    Session session = unit.get(Session.class);
    expect(session.executeAsync(unit.capture(Statement.class))).andReturn(rs);

    unit.mockStatic(Futures.class);
    Futures.addCallback(eq(rs), unit.capture(FutureCallback.class));
  };

  private Block boundStatement = unit -> {
    BoundStatement statement = unit.constructor(BoundStatement.class)
        .args(PreparedStatement.class)
        .build(unit.get(PreparedStatement.class));

    unit.registerMock(BoundStatement.class, statement);
  };

  @Test
  public void newSessionStore30m() throws Exception {
    new MockUnit(Session.class)
        .expect(createTable)
        .run(unit -> {
          new CassandraSessionStore(unit.get(Session.class), "30m");
        });
  }

  @Test
  public void createTableStatement() throws Exception {
    new MockUnit(Session.class)
        .expect(createTable)
        .run(unit -> {
          new CassandraSessionStore(unit.get(Session.class), "30m");
        }, unit -> {
          String statement = unit.captured(Statement.class).iterator().next().toString();
          assertEquals("\n" +
              " CREATE TABLE IF NOT EXISTS session(\n" +
              "  id varchar,\n" +
              "  createdAt timestamp,\n" +
              "  accessedAt timestamp,\n" +
              "  savedAt timestamp,\n" +
              "  attributes map<varchar, varchar>,\n" +
              "  PRIMARY KEY(id))", statement.replace('\t', ' '));
        });
  }

  @SuppressWarnings({"unchecked", "rawtypes" })
  @Test
  public void createTableSuccess() throws Exception {
    new MockUnit(Session.class, ResultSet.class)
        .expect(createTable)
        .run(unit -> {
          new CassandraSessionStore(unit.get(Session.class), "30m");
        }, unit -> {
          FutureCallback success = unit.captured(FutureCallback.class).iterator().next();
          success.onSuccess(unit.get(ResultSet.class));
        });
  }

  @SuppressWarnings("rawtypes")
  @Test
  public void createTableFailure() throws Exception {
    new MockUnit(Session.class, ResultSet.class)
        .expect(createTable)
        .run(unit -> {
          new CassandraSessionStore(unit.get(Session.class), "30m");
        }, unit -> {
          FutureCallback success = unit.captured(FutureCallback.class).iterator().next();
          success.onFailure(new IllegalStateException("intentional err"));
        });
  }

  @Test
  public void newSessionStore30s() throws Exception {
    new MockUnit(Session.class)
        .expect(createTable)
        .run(unit -> {
          new CassandraSessionStore(unit.get(Session.class), "30");
        });
  }

  @Test
  public void save() throws Exception {
    new MockUnit(Session.class, org.jooby.Session.class)
        .expect(createTable)
        .expect(session("sid", 1, 2, 3, ImmutableMap.of("foo", "bar")))
        .expect(insertInto(1800))
        .expect(boundStatement)
        .expect(unit -> {
          BoundStatement statement = unit.get(BoundStatement.class);

          expect(statement.bind("sid", new Date(1), new Date(2), new Date(3),
              ImmutableMap.of("foo", "bar"))).andReturn(statement);

          Session session = unit.get(Session.class);
          expect(session.execute(statement)).andReturn(null);
        })
        .run(unit -> {
          new CassandraSessionStore(unit.get(Session.class), "30m")
              .save(unit.get(org.jooby.Session.class));
        });
  }

  @Test
  public void create() throws Exception {
    new MockUnit(Session.class, org.jooby.Session.class)
        .expect(createTable)
        .expect(session("sid", 5, 6, 7, ImmutableMap.of("foo", "bar")))
        .expect(insertInto(0))
        .expect(boundStatement)
        .expect(unit -> {
          BoundStatement statement = unit.get(BoundStatement.class);

          expect(statement.bind("sid", new Date(5), new Date(6), new Date(7),
              ImmutableMap.of("foo", "bar"))).andReturn(statement);

          Session session = unit.get(Session.class);
          expect(session.execute(statement)).andReturn(null);
        })
        .run(unit -> {
          new CassandraSessionStore(unit.get(Session.class), 0)
              .create(unit.get(org.jooby.Session.class));
        });
  }

  @Test
  public void get() throws Exception {
    new MockUnit(Session.class, org.jooby.Session.class, org.jooby.Session.Builder.class,
        ResultSet.class)
            .expect(createTable)
            .expect(unit -> {
              PreparedStatement statement = unit.mock(PreparedStatement.class);
              unit.registerMock(PreparedStatement.class, statement);

              Session session = unit.get(Session.class);

              expect(session.prepare("SELECT * FROM session WHERE id=?;")).andReturn(statement);
            })
            .expect(unit -> {
              Builder builder = unit.get(org.jooby.Session.Builder.class);
              expect(builder.sessionId()).andReturn("sid");
            })
            .expect(boundStatement)
            .expect(unit -> {
              BoundStatement statement = unit.get(BoundStatement.class);

              expect(statement.bind("sid")).andReturn(statement);

              Session session = unit.get(Session.class);
              expect(session.execute(statement)).andReturn(unit.get(ResultSet.class));
            })
            .expect(unit -> {
              Row row = unit.mock(Row.class);
              Date createdAt = new Date();
              Date accessedAt = new Date();
              Date savedAt = new Date();
              expect(row.getTimestamp("createdAt")).andReturn(createdAt);
              expect(row.getTimestamp("accessedAt")).andReturn(accessedAt);
              expect(row.getTimestamp("savedAt")).andReturn(savedAt);
              expect(row.getMap("attributes", String.class, String.class))
                  .andReturn(ImmutableMap.of("foo", "bar"));

              Builder builder = unit.get(org.jooby.Session.Builder.class);
              expect(builder.createdAt(createdAt.getTime())).andReturn(builder);
              expect(builder.accessedAt(accessedAt.getTime())).andReturn(builder);
              expect(builder.savedAt(savedAt.getTime())).andReturn(builder);
              expect(builder.set(ImmutableMap.of("foo", "bar"))).andReturn(builder);
              expect(builder.build()).andReturn(unit.get(org.jooby.Session.class));

              ResultSet rs = unit.get(ResultSet.class);
              expect(rs.one()).andReturn(row);
            })
            .expect(session("sid", 1, 2, 3, ImmutableMap.of("foo", "bar")))
            .expect(insertInto(30))
            .expect(boundStatement)
            .expect(unit -> {
              BoundStatement statement = unit.get(BoundStatement.class);

              expect(statement.bind("sid", new Date(1), new Date(2), new Date(3),
                  ImmutableMap.of("foo", "bar"))).andReturn(statement);

              Session session = unit.get(Session.class);
              expect(session.execute(statement)).andReturn(null);
            })
            .run(unit -> {
              new CassandraSessionStore(unit.get(Session.class), "30")
                  .get(unit.get(org.jooby.Session.Builder.class));
            });
  }

  @Test
  public void delete() throws Exception {
    new MockUnit(Session.class, org.jooby.Session.class)
        .expect(createTable)
        .expect(unit -> {
          PreparedStatement statement = unit.mock(PreparedStatement.class);
          unit.registerMock(PreparedStatement.class, statement);

          Session session = unit.get(Session.class);

          expect(session.prepare("DELETE FROM session WHERE id=?;")).andReturn(statement);
        })
        .expect(boundStatement)
        .expect(unit -> {
          BoundStatement statement = unit.get(BoundStatement.class);

          expect(statement.bind("sid")).andReturn(statement);

          Session session = unit.get(Session.class);
          expect(session.execute(statement)).andReturn(null);
        })
        .run(unit -> {
          new CassandraSessionStore(unit.get(Session.class), "30")
              .delete("sid");
        });
  }

  @Test
  public void getWithoutTtl() throws Exception {
    new MockUnit(Session.class, org.jooby.Session.class, org.jooby.Session.Builder.class,
        ResultSet.class)
            .expect(createTable)
            .expect(unit -> {
              PreparedStatement statement = unit.mock(PreparedStatement.class);
              unit.registerMock(PreparedStatement.class, statement);

              Session session = unit.get(Session.class);

              expect(session.prepare("SELECT * FROM session WHERE id=?;")).andReturn(statement);
            })
            .expect(unit -> {
              Builder builder = unit.get(org.jooby.Session.Builder.class);
              expect(builder.sessionId()).andReturn("sid");
            })
            .expect(boundStatement)
            .expect(unit -> {
              BoundStatement statement = unit.get(BoundStatement.class);

              expect(statement.bind("sid")).andReturn(statement);

              Session session = unit.get(Session.class);
              expect(session.execute(statement)).andReturn(unit.get(ResultSet.class));
            })
            .expect(unit -> {
              Row row = unit.mock(Row.class);
              Date createdAt = new Date();
              Date accessedAt = new Date();
              Date savedAt = new Date();
              expect(row.getTimestamp("createdAt")).andReturn(createdAt);
              expect(row.getTimestamp("accessedAt")).andReturn(accessedAt);
              expect(row.getTimestamp("savedAt")).andReturn(savedAt);
              expect(row.getMap("attributes", String.class, String.class))
                  .andReturn(ImmutableMap.of("foo", "bar"));

              Builder builder = unit.get(org.jooby.Session.Builder.class);
              expect(builder.createdAt(createdAt.getTime())).andReturn(builder);
              expect(builder.accessedAt(accessedAt.getTime())).andReturn(builder);
              expect(builder.savedAt(savedAt.getTime())).andReturn(builder);
              expect(builder.set(ImmutableMap.of("foo", "bar"))).andReturn(builder);
              expect(builder.build()).andReturn(unit.get(org.jooby.Session.class));

              ResultSet rs = unit.get(ResultSet.class);
              expect(rs.one()).andReturn(row);
            })
            .run(unit -> {
              new CassandraSessionStore(unit.get(Session.class), "0")
                  .get(unit.get(org.jooby.Session.Builder.class));
            });
  }

  private Block insertInto(final int ttl) {
    return unit -> {
      PreparedStatement statement = unit.mock(PreparedStatement.class);
      unit.registerMock(PreparedStatement.class, statement);

      Session session = unit.get(Session.class);
      String suffix = ttl > 0 ? " USING TTL " + ttl : "";
      expect(session.prepare(
          "INSERT INTO session (id,createdAt,accessedAt,savedAt,attributes) VALUES (?,?,?,?,?)"
              + suffix + ";"))
                  .andReturn(statement);
    };
  }

  private Block session(final String sid, final long createdAt, final long accessedAt,
      final long savedAt, final Map<String, String> attributes) {
    return unit -> {
      org.jooby.Session session = unit.get(org.jooby.Session.class);
      expect(session.id()).andReturn(sid);
      expect(session.createdAt()).andReturn(createdAt);
      expect(session.accessedAt()).andReturn(accessedAt);
      expect(session.savedAt()).andReturn(savedAt);
      expect(session.attributes()).andReturn(attributes);
    };
  }

}
