/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.metron.indexing.integration;

import org.apache.metron.hbase.mock.MockHBaseTableProvider;
import org.apache.metron.indexing.dao.AccessConfig;
import org.apache.metron.indexing.dao.HBaseDao;
import org.apache.metron.indexing.dao.IndexDao;
import org.apache.metron.indexing.dao.UpdateIntegrationTest;
import org.apache.metron.indexing.dao.search.AlertComment;
import org.apache.metron.indexing.dao.search.GetRequest;
import org.apache.metron.indexing.dao.update.Document;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

import static org.apache.metron.indexing.dao.HBaseDao.HBASE_CF;
import static org.apache.metron.indexing.dao.HBaseDao.HBASE_TABLE;
import static org.apache.metron.indexing.dao.IndexDao.COMMENTS_FIELD;
import static org.junit.jupiter.api.Assertions.*;

public class HBaseDaoIntegrationTest extends UpdateIntegrationTest  {

  private static final String TABLE_NAME = "metron_update";
  private static final String COLUMN_FAMILY = "cf";
  private static final String SENSOR_TYPE = "test";

  private static IndexDao hbaseDao;
  private static byte[] expectedKeySerialization = new byte[] {
          (byte)0xf5,0x53,0x76,(byte)0x96,0x67,0x3a,
          (byte)0xc1,(byte)0xaf,(byte)0xff,0x41,0x33,(byte)0x9d,
          (byte)0xac,(byte)0xb9,0x1a,(byte)0xb0,0x00,0x04,
          0x67,0x75,0x69,0x64,0x00,0x0a,
          0x73,0x65,0x6e,0x73,0x6f,0x72,
          0x54,0x79,0x70,0x65
  };

  @BeforeEach
  public void startHBase() {
    AccessConfig accessConfig = new AccessConfig();
    accessConfig.setMaxSearchResults(1000);
    accessConfig.setMaxSearchGroups(1000);
    accessConfig.setGlobalConfigSupplier(() -> new HashMap<String, Object>() {{
      put(HBASE_TABLE, TABLE_NAME);
      put(HBASE_CF, COLUMN_FAMILY);
    }});
    MockHBaseTableProvider.addToCache(TABLE_NAME, COLUMN_FAMILY);
    accessConfig.setTableProvider(new MockHBaseTableProvider());

    hbaseDao = new HBaseDao();
    hbaseDao.init(accessConfig);
  }

  @AfterEach
  public void clearTable() {
    MockHBaseTableProvider.clear();
  }

  /**
   * IF this test fails then you have broken the key serialization in that your change has
   * caused a key to change serialization, so keys from previous releases will not be able to be found
   * under your scheme.  Please either provide a migration plan or undo this change.  DO NOT CHANGE THIS
   * TEST BLITHELY!
   * @throws IOException
   */
  @Test
  public void testKeySerializationRemainsConstant() throws IOException {
    HBaseDao.Key k = new HBaseDao.Key("guid", "sensorType");
    byte[] raw = k.toBytes();
    assertArrayEquals(raw, expectedKeySerialization);
  }


  @Test
  public void testKeySerialization() throws Exception {
    HBaseDao.Key k = new HBaseDao.Key("guid", "sensorType");
    assertEquals(k, HBaseDao.Key.fromBytes(HBaseDao.Key.toBytes(k)));
  }

  @Test
  public void testKeySerializationWithInvalidGuid() {
    HBaseDao.Key k = new HBaseDao.Key(null, "sensorType");
    assertThrows(IllegalStateException.class, () ->  HBaseDao.Key.fromBytes(HBaseDao.Key.toBytes(k)));
  }

  @Test
  public void testKeySerializationWithInvalidSensorType() {
    HBaseDao.Key k = new HBaseDao.Key("guid", null);
    assertThrows(IllegalStateException.class, () -> HBaseDao.Key.fromBytes(HBaseDao.Key.toBytes(k)));
  }

  @Test
  public void shouldGetLatest() throws Exception {
    // Load alerts
    List<Document> alerts = buildAlerts(3);
    Map<Document, Optional<String>> updates = alerts.stream()
        .collect(Collectors.toMap(document -> document, document -> Optional.empty()));
    hbaseDao.batchUpdate(updates);

    Document actualDocument = hbaseDao.getLatest("message_1", SENSOR_TYPE);
    Document expectedDocument = alerts.get(1);
    assertEquals(expectedDocument, actualDocument);
  }

  @Test
  public void shouldGetLatestWithInvalidTimestamp() throws Exception {
    // Load alert
    Document alert = buildAlerts(1).get(0);
    hbaseDao.update(alert, Optional.empty());

    Document actualDocument = hbaseDao.getLatest("message_0", SENSOR_TYPE);
    assertEquals(alert, actualDocument);

    alert.getDocument().put("field", "value");
    alert.setTimestamp(0L);
    hbaseDao.update(alert, Optional.empty());

    actualDocument = hbaseDao.getLatest("message_0", SENSOR_TYPE);
    assertEquals(alert.getDocument(), actualDocument.getDocument());
  }

  @Test
  public void shouldGetAllLatest() throws Exception {
    // Load alerts
    List<Document> alerts = buildAlerts(15);
    alerts.stream().collect(Collectors.toMap(Document::getGuid, document -> Optional.empty()));
    Map<Document, Optional<String>> updates = alerts.stream()
        .collect(Collectors.toMap(document -> document, document -> Optional.empty()));
    hbaseDao.batchUpdate(updates);

    int expectedCount = 12;
    List<GetRequest> getRequests = new ArrayList<>();
    for(int i = 1; i < expectedCount + 1; i ++) {
      getRequests.add(new GetRequest("message_" + i, SENSOR_TYPE));
    }
    Iterator<Document> results = hbaseDao.getAllLatest(getRequests).iterator();

    for (int i = 0; i < expectedCount; i++) {
      Document expectedDocument = alerts.get(i + 1);
      Document actualDocument = results.next();
      assertEquals(expectedDocument, actualDocument);
    }

    assertFalse(results.hasNext(), "Result size should be 12 but was greater");
  }

  protected List<Document> buildAlerts(int count) throws IOException {
    List<Document> alerts = new ArrayList<>();
    for (int i = 0; i < count; ++i) {
      String guid = "message_" + i;
      String json = "{\"guid\":\"message_" + i + "\", \"source:type\":\"test\"}";
      Document alert = new Document(json, guid, SENSOR_TYPE, System.currentTimeMillis());
      alerts.add(alert);
    }
    return alerts;
  }

  @Test
  public void testRemoveComments() throws Exception {
    Map<String, Object> fields = new HashMap<>();
    fields.put("guid", "add_comment");
    fields.put("source.type", SENSOR_NAME);

    Document document = new Document(fields, "add_comment", SENSOR_NAME, 1526401584951L);
    hbaseDao.update(document, Optional.of(SENSOR_NAME));
    findUpdatedDoc(document.getDocument(), "add_comment", SENSOR_NAME);

    addAlertComment("add_comment", "New Comment", "test_user", 1526401584951L);
    // Ensure we have the first comment
    ArrayList<AlertComment> comments = new ArrayList<>();
    comments.add(new AlertComment("New Comment", "test_user", 1526401584951L));
    document.getDocument().put(COMMENTS_FIELD, comments.stream().map(AlertComment::asMap).collect(
        Collectors.toList()));
    findUpdatedDoc(document.getDocument(), "add_comment", SENSOR_NAME);

    addAlertComment("add_comment", "New Comment 2", "test_user_2", 1526401584952L);
    // Ensure we have the second comment
    comments.add(new AlertComment("New Comment 2", "test_user_2", 1526401584952L));
    document.getDocument().put(COMMENTS_FIELD, comments.stream().map(AlertComment::asMap).collect(
        Collectors.toList()));
    findUpdatedDoc(document.getDocument(), "add_comment", SENSOR_NAME);

    removeAlertComment("add_comment", "New Comment 2", "test_user_2", 1526401584952L);
    // Ensure we only have the first comments
    comments = new ArrayList<>();
    comments.add(new AlertComment(commentOne));
    document.getDocument().put(COMMENTS_FIELD, comments.stream().map(AlertComment::asMap).collect(
        Collectors.toList()));
    findUpdatedDoc(document.getDocument(), "add_comment", SENSOR_NAME);

    removeAlertComment("add_comment", "New Comment", "test_user", 1526401584951L);
    // Ensure we have no comments
    document.getDocument().remove(COMMENTS_FIELD);
    findUpdatedDoc(document.getDocument(), "add_comment", SENSOR_NAME);
  }

  @Override
  protected IndexDao getDao() {
    return hbaseDao;
  }

  @Override
  protected String getIndexName() {
    return null;
  }

  @Override
  protected void addTestData(String indexName, String sensorType, List<Map<String, Object>> docs) {
  }

  @Override
  protected List<Map<String, Object>> getIndexedTestData(String indexName, String sensorType) {
    return null;
  }
}
