package org.jooby.mongodb;

import com.google.common.collect.ImmutableMap;
import com.mongodb.DBObject;
import com.mongodb.client.FindIterable;
import com.mongodb.client.ListIndexesIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.IndexOptions;
import com.mongodb.client.model.UpdateOptions;
import com.mongodb.client.result.UpdateResult;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.jooby.Session;
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 java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;

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

@RunWith(PowerMockRunner.class)
@PrepareForTest({MongoSessionStore.class, IndexOptions.class, UpdateOptions.class, Filters.class,
    LinkedHashMap.class})
public class MongodbSessionStoreTest {

  @SuppressWarnings({"unchecked", "rawtypes"})
  MockUnit.Block boot = unit -> {
    MongoCollection collection = unit.get(MongoCollection.class);

    MongoDatabase db = unit.get(MongoDatabase.class);
    expect(db.getCollection("sess")).andReturn(collection);
  };

  long now = System.currentTimeMillis();

  Map<String, String> attrs = ImmutableMap.<String, String>of("k.v", "v", "$d", "d");

  @SuppressWarnings("rawtypes")
  MockUnit.Block saveSession = unit -> {

    MongoCollection collection = unit.get(MongoCollection.class);

    Session session = unit.get(Session.class);
    expect(session.id()).andReturn("1234");
    expect(session.accessedAt()).andReturn(now);
    expect(session.createdAt()).andReturn(now);
    expect(session.savedAt()).andReturn(now);
    expect(session.attributes()).andReturn(attrs);

    UpdateResult result = unit.mock(UpdateResult.class);
    Document doc = new Document()
        .append("_id", "1234")
        .append("_accessedAt", new Date(now))
        .append("_createdAt", new Date(now))
        .append("_savedAt", new Date(now))
        .append("k\uFF0Ev", "v")
        .append("\uFF04d", "d");

    UpdateOptions options = unit.constructor(UpdateOptions.class)
        .build();
    expect(options.upsert(true)).andReturn(options);

    Bson eq = unit.mock(Bson.class);
    unit.mockStatic(Filters.class);
    expect(Filters.eq("_id", "1234")).andReturn(eq);

    expect(collection.updateOne(eq, new Document("$set", doc), options))
        .andReturn(result);
  };

  @SuppressWarnings("rawtypes")
  private Block noIndexes = unit -> {
    MongoCursor cursor = unit.mock(MongoCursor.class);
    expect(cursor.hasNext()).andReturn(false);

    ListIndexesIterable lii = unit.mock(ListIndexesIterable.class);
    expect(lii.iterator()).andReturn(cursor);

    MongoCollection coll = unit.get(MongoCollection.class);
    expect(coll.listIndexes()).andReturn(lii);
  };

  @SuppressWarnings("rawtypes")
  private Block indexes = unit -> {

    Document d1 = unit.mock(Document.class);
    expect(d1.getString("name")).andReturn("n1");

    Document d2 = unit.mock(Document.class);
    expect(d2.getString("name")).andReturn("_sessionIdx_");

    MongoCursor cursor = unit.mock(MongoCursor.class);
    expect(cursor.hasNext()).andReturn(true);
    expect(cursor.next()).andReturn(d1);
    expect(cursor.hasNext()).andReturn(true);
    expect(cursor.next()).andReturn(d2);

    ListIndexesIterable lii = unit.mock(ListIndexesIterable.class);
    expect(lii.iterator()).andReturn(cursor);

    MongoCollection coll = unit.get(MongoCollection.class);
    expect(coll.listIndexes()).andReturn(lii);
  };

  private Block runCommand = unit -> {
    MongoDatabase db = unit.get(MongoDatabase.class);
    Document command = new Document("collMod", "sess")
        .append("index",
            new Document("keyPattern", new Document("_accessedAt", 1))
                .append("expireAfterSeconds", 60L));

    expect(db.runCommand(command)).andReturn(unit.mock(Document.class));
  };

  @Test
  public void defaults() throws Exception {
    new MockUnit(MongoDatabase.class, MongoCollection.class)
        .expect(boot)
        .run(unit -> {
          new MongoSessionStore(unit.get(MongoDatabase.class), "sess", "1m");
        });
  }

  @Test(expected = NullPointerException.class)
  public void defaultsNullDB() throws Exception {
    new MongoSessionStore(null, "sess", "1m");
  }

  @SuppressWarnings("rawtypes")
  @Test
  public void create() throws Exception {
    new MockUnit(Session.class, MongoDatabase.class, MongoCollection.class)
        .expect(boot)
        .expect(saveSession)
        .expect(noIndexes)
        .expect(unit -> {
          MongoCollection collection = unit.get(MongoCollection.class);
          IndexOptions options = unit.constructor(IndexOptions.class)
              .build();
          expect(options.name("_sessionIdx_")).andReturn(options);
          expect(options.expireAfter(300L, TimeUnit.SECONDS)).andReturn(options);
          expect(collection.createIndex(new Document("_accessedAt", 1), options))
              .andReturn("idx");
        })
        .run(unit -> {
          new MongoSessionStore(unit.get(MongoDatabase.class), "sess", "5m")
              .create(unit.get(Session.class));
          ;
        });
  }

  @Test
  public void save() throws Exception {
    new MockUnit(Session.class, MongoDatabase.class, MongoCollection.class)
        .expect(boot)
        .expect(indexes)
        .expect(saveSession)
        .expect(runCommand)
        .run(unit -> {
          new MongoSessionStore(unit.get(MongoDatabase.class), "sess", "60")
              .save(unit.get(Session.class));
          ;
        });
  }

  @SuppressWarnings("rawtypes")
  @Test
  public void shouldSyncTtlOnce() throws Exception {
    new MockUnit(Session.class, MongoDatabase.class, MongoCollection.class)
        .expect(boot)
        .expect(saveSession)
        .expect(noIndexes)
        .expect(saveSession)
        .expect(unit -> {
          MongoCollection collection = unit.get(MongoCollection.class);
          IndexOptions options = unit.constructor(IndexOptions.class)
              .build();
          expect(options.name("_sessionIdx_")).andReturn(options);
          expect(options.expireAfter(60L, TimeUnit.SECONDS)).andReturn(options);
          expect(collection.createIndex(new Document("_accessedAt", 1), options))
              .andReturn("idx");
        })
        .run(unit -> {
          MongoSessionStore mongodb = new MongoSessionStore(unit.get(MongoDatabase.class), "sess",
              "60");
          mongodb.save(unit.get(Session.class));
          mongodb.save(unit.get(Session.class));
        });
  }

  @Test
  public void saveNoTimeout() throws Exception {
    new MockUnit(Session.class, MongoDatabase.class, MongoCollection.class)
        .expect(boot)
        .expect(saveSession)
        .run(unit -> {
          new MongoSessionStore(unit.get(MongoDatabase.class), "sess", "0")
              .save(unit.get(Session.class));
          ;
        });
  }

  @Test
  public void saveSyncTtl() throws Exception {
    new MockUnit(Session.class, MongoDatabase.class, MongoCollection.class)
        .expect(boot)
        .expect(saveSession)
        .expect(indexes)
        .expect(runCommand)
        .run(unit -> {
          new MongoSessionStore(unit.get(MongoDatabase.class), "sess", "60")
              .save(unit.get(Session.class));
          ;
        });
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  @Test
  public void get() throws Exception {
    long now = System.currentTimeMillis();
    new MockUnit(Session.class, Session.Builder.class, MongoDatabase.class, MongoCollection.class,
        DBObject.class)
        .expect(boot)
        .expect(unit -> {
          Document doc = unit.mock(Document.class);

          Map sessionMap = unit.constructor(LinkedHashMap.class)
              .args(Map.class)
              .build(doc);
          expect(sessionMap.remove("_accessedAt")).andReturn(new Date(now));
          expect(sessionMap.remove("_createdAt")).andReturn(new Date(now));
          expect(sessionMap.remove("_savedAt")).andReturn(new Date(now));
          expect(sessionMap.remove("_id")).andReturn("1234");
          sessionMap.forEach(unit.capture(BiConsumer.class));

          FindIterable result = unit.mock(FindIterable.class);
          expect(result.first()).andReturn(doc);

          Session.Builder sb = unit.get(Session.Builder.class);
          expect(sb.sessionId()).andReturn("1234");
          expect(sb.accessedAt(now)).andReturn(sb);
          expect(sb.createdAt(now)).andReturn(sb);
          expect(sb.savedAt(now)).andReturn(sb);
          expect(sb.set("a.b", "c")).andReturn(sb);
          expect(sb.build()).andReturn(unit.get(Session.class));

          Bson eq = unit.mock(Bson.class);
          unit.mockStatic(Filters.class);
          expect(Filters.eq("_id", "1234")).andReturn(eq);

          MongoCollection collection = unit.get(MongoCollection.class);
          expect(collection.find(eq)).andReturn(result);
        })
        .run(unit -> {
          MongoSessionStore mss = new MongoSessionStore(unit.get(MongoDatabase.class), "sess",
              "60");
          assertEquals(unit.get(Session.class), mss.get(unit.get(Session.Builder.class)));
        }, unit -> {
          BiConsumer<String, String> setter = unit.captured(BiConsumer.class).get(0);
          setter.accept("a\uFF0Eb", "c");
        });
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  @Test
  public void getDollar() throws Exception {
    long now = System.currentTimeMillis();
    new MockUnit(Session.class, Session.Builder.class, MongoDatabase.class, MongoCollection.class,
        DBObject.class)
        .expect(boot)
        .expect(unit -> {
          Document doc = unit.mock(Document.class);

          Map sessionMap = unit.constructor(LinkedHashMap.class)
              .args(Map.class)
              .build(doc);
          expect(sessionMap.remove("_accessedAt")).andReturn(new Date(now));
          expect(sessionMap.remove("_createdAt")).andReturn(new Date(now));
          expect(sessionMap.remove("_savedAt")).andReturn(new Date(now));
          expect(sessionMap.remove("_id")).andReturn("1234");
          sessionMap.forEach(unit.capture(BiConsumer.class));

          FindIterable result = unit.mock(FindIterable.class);
          expect(result.first()).andReturn(doc);

          Session.Builder sb = unit.get(Session.Builder.class);
          expect(sb.sessionId()).andReturn("1234");
          expect(sb.accessedAt(now)).andReturn(sb);
          expect(sb.createdAt(now)).andReturn(sb);
          expect(sb.savedAt(now)).andReturn(sb);
          expect(sb.set("$ab", "c")).andReturn(sb);
          expect(sb.build()).andReturn(unit.get(Session.class));

          Bson eq = unit.mock(Bson.class);
          unit.mockStatic(Filters.class);
          expect(Filters.eq("_id", "1234")).andReturn(eq);

          MongoCollection collection = unit.get(MongoCollection.class);
          expect(collection.find(eq)).andReturn(result);
        })
        .run(unit -> {
          MongoSessionStore mss = new MongoSessionStore(unit.get(MongoDatabase.class), "sess",
              "60");
          assertEquals(unit.get(Session.class), mss.get(unit.get(Session.Builder.class)));
        }, unit -> {
          BiConsumer<String, String> setter = unit.captured(BiConsumer.class).get(0);
          setter.accept("\uFF04ab", "c");
        });
  }

  @SuppressWarnings("rawtypes")
  @Test
  public void getExpired() throws Exception {
    new MockUnit(Session.class, Session.Builder.class, MongoDatabase.class, MongoCollection.class)
        .expect(boot)
        .expect(unit -> {
          Session.Builder sb = unit.get(Session.Builder.class);
          expect(sb.sessionId()).andReturn("1234");

          Bson eq = unit.mock(Bson.class);
          unit.mockStatic(Filters.class);
          expect(Filters.eq("_id", "1234")).andReturn(eq);

          FindIterable result = unit.mock(FindIterable.class);
          expect(result.first()).andReturn(null);

          MongoCollection collection = unit.get(MongoCollection.class);
          expect(collection.find(eq)).andReturn(result);
        })
        .run(unit -> {
          assertEquals(null,
              new MongoSessionStore(unit.get(MongoDatabase.class), "sess", "60")
                  .get(unit.get(Session.Builder.class)));
        });
  }

  @SuppressWarnings("rawtypes")
  @Test
  public void delete() throws Exception {
    new MockUnit(MongoDatabase.class, MongoCollection.class)
        .expect(boot)
        .expect(unit -> {
          MongoCollection collection = unit.get(MongoCollection.class);
          expect(collection.deleteOne(new Document("_id", "1234"))).andReturn(null);
        })
        .run(unit -> {
          new MongoSessionStore(unit.get(MongoDatabase.class), "sess", "60")
              .delete("1234");
        });
  }

}
