/*
 * (c) 2021 Pluto Inc.
 */
package tv.pluto;

import static com.mongodb.client.model.Projections.include;
import static tv.pluto.ip.dal.appname.AppNames.AppName;

import com.plutotv.test.lib.LogUtils;
import io.grpc.ManagedChannel;
import io.grpc.StatusRuntimeException;
import io.qameta.allure.Description;
import io.qameta.allure.TmsLink;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.UUID;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import tv.pluto.core.BaseTest;
import tv.pluto.grpc.app_names.AppNamesClient;
import tv.pluto.ip.dal.appname.AppNames.GetAllPagedResponse;
import tv.pluto.mongo.dto.ContentFeedQuery;
import tv.pluto.mongo.entities.appnames.AppNames;
import tv.pluto.mongo.utils.AppNamesUtils;

public class AppNamesTestSuite extends BaseTest {
  private AppNamesClient appNamesClient;
  private AppNamesClient appNamesClientAuth;
  private AppNamesUtils appNamesUtils;
  private List<AppName> appNameList;

  @BeforeClass(alwaysRun = true)
  private void setUp() {
    ManagedChannel channel = grpcChannelManager.getChannel();
    appNamesClient = new AppNamesClient(channel, false);
    appNamesClientAuth = new AppNamesClient(channel, true);
    appNamesUtils = new AppNamesUtils();
    appNameList = new ArrayList<>();
  }

  @Test
  @Description("GetDocumentsUpdatedBetweenDates - Time period for which there is a application")
  @TmsLink("28500370")
  public void C28500370_TEST() {

    ContentFeedQuery contentFeedQuery =
        ContentFeedQuery.builder()
            .updatedAtMin(appNamesUtils.updatedAtMin)
            .updatedAtMax(appNamesUtils.updatedAtMax)
            .limit(0)
            .projections(include("id"))
            .build();

    List<AppNames> mongoAppNames = appNamesUtils.getDocumentsForContentFeeds(contentFeedQuery);
    List<AppNames> grpcAppNames = new ArrayList<>();

    Iterator<AppName> appNames =
        appNamesClient.getDocumentsUpdatedBetweenDates(
            appNamesUtils.updatedAtMinString, appNamesUtils.updatedAtMaxString);

    while (appNames.hasNext()) {
      grpcAppNames.add(appNamesUtils.mapGrpcObjectToPojo(appNames.next(), AppNames.class));
    }

    mongoAppNames.sort(Comparator.comparing(AppNames::getId));
    grpcAppNames.sort(Comparator.comparing(AppNames::getId));

    List<String> mongoIds = mongoAppNames.stream().map(AppNames::getId).toList();
    List<String> grpcIds = grpcAppNames.stream().map(AppNames::getId).toList();

    Assert.assertEquals(
        mongoIds, grpcIds, "The IDs do not match between Mongo and gRPC app names.");
  }

  @Test(
      expectedExceptions = StatusRuntimeException.class,
      expectedExceptionsMessageRegExp = NOT_FOUND_MESSAGE)
  @Description("GetDocumentsUpdatedBetweenDates - Time period without application")
  @TmsLink("28500371")
  public void C28500371_TEST() {
    appNamesClient.getDocumentsUpdatedBetweenDates(failStartDateTime, failEndDateTime).next();
  }

  @Test(
      dataProvider = "fail-content-feeds-test-data",
      expectedExceptions = StatusRuntimeException.class,
      expectedExceptionsMessageRegExp = INVALID_ARGUMENT_MESSAGE)
  @TmsLink("28500372")
  @Description(
      "GetDocumentsUpdatedBetweenDates - Invalid values for start_date_time and end_date_time")
  public void C28500372_TEST(String startDateTime, String endDateTime) {
    appNamesClient.getDocumentsUpdatedBetweenDates(startDateTime, endDateTime).next();
  }

  @Test(
      dataProvider = "less-or-equal-fail-content-feeds-test-data",
      expectedExceptions = StatusRuntimeException.class,
      expectedExceptionsMessageRegExp = INTERNAL_ERROR_MESSAGE)
  @TmsLink("28500373")
  @Description(
      "GetDocumentsUpdatedBetweenDates - end_date_time is less than or equal to start_date_time")
  public void C28500373_TEST(String startDateTime, String endDateTime) {
    appNamesClient.getDocumentsUpdatedBetweenDates(startDateTime, endDateTime).next();
  }

  @Test
  @Description("GetByDeviceIdRequest - verify device_id objects")
  @TmsLink("28500382")
  public void C28500382_TEST() {
    String device = appNamesUtils.getDistinctRandomValue("devices");
    LogUtils.logInfoMessage("Device: " + device);

    List<tv.pluto.ip.dal.appname.AppNames> listDeviceIds =
        appNamesClient.getByDeviceIdRequest(device);
    LogUtils.logInfoMessage("Device Ids List: " + listDeviceIds);
    List<AppNames> listGrpAppNames =
        appNamesUtils.mapGrpcObjectsToPojo(listDeviceIds, AppNames.class);
    List<AppNames> listMongoAppNames = appNamesUtils.getDocumentsByFieldAndValue("devices", device);

    listGrpAppNames.sort(Comparator.comparing(AppNames::getId));
    listMongoAppNames.sort(Comparator.comparing(AppNames::getId));

    Assert.assertFalse(listGrpAppNames.isEmpty());
    Assert.assertEquals(listGrpAppNames, listMongoAppNames);
  }

  @Test
  @Description("Device - added a device's array in the response")
  @TmsLink("28500385")
  public void C28500385_TEST() {
    String device = appNamesUtils.getDistinctRandomValue("devices");
    LogUtils.logInfoMessage("Device: " + device);

    List<tv.pluto.ip.dal.appname.AppNames> listDeviceIds =
        appNamesClient.getByDeviceIdRequest(device);
    List<AppNames> listGrpAppNames =
        appNamesUtils.mapGrpcObjectsToPojo(listDeviceIds, AppNames.class);

    boolean isDevices = listGrpAppNames.stream().allMatch(a -> a.getDevices().contains(device));
    Assert.assertTrue(isDevices);
  }

  @Test
  @Description("GetAll - verify a devicesrev field")
  @TmsLink("28500387")
  public void C28500387_TEST() {
    try {
      List<AppNames> listDevices = appNamesUtils.getDocuments(999);
      AppNames appName =
          listDevices.stream().filter(app -> app.getDevicesrev().size() > 0).findAny().get();

      List<tv.pluto.ip.dal.appname.AppNames.AppName> getAllAppNameList = appNamesClient.getAll();
      AppName appNameGrpc =
          getAllAppNameList.stream()
              .filter(app -> app.getId().equals(appName.getId()))
              .findAny()
              .get();
      AppNames appNameFromGrpc = appNamesUtils.mapGrpcObjectToPojo(appNameGrpc, AppNames.class);

      Assert.assertEquals(appName, appNameFromGrpc);
      Assert.assertTrue(appName.getDevicesrev().size() > 0);
    } catch (NoSuchElementException e) {
      LogUtils.logSevereMessage("No devicesrev data in AppNames");
    }
  }

  @Test
  @Description("GetAll - verify a regions array")
  @TmsLink("28500388")
  public void C28500388_TEST() {
    try {
      List<AppNames> listDevices = appNamesUtils.getDocuments(999);
      AppNames appName =
          listDevices.stream().filter(app -> app.getRegions().size() > 0).findAny().get();

      List<tv.pluto.ip.dal.appname.AppNames.AppName> getAllAppNameList = appNamesClient.getAll();
      AppName appNameGrpc =
          getAllAppNameList.stream()
              .filter(app -> app.getId().equals(appName.getId()))
              .findAny()
              .get();
      AppNames appNameFromGrpc = appNamesUtils.mapGrpcObjectToPojo(appNameGrpc, AppNames.class);

      Assert.assertEquals(appName, appNameFromGrpc);
      Assert.assertTrue(appName.getDevices().size() > 0);
    } catch (NoSuchElementException e) {
      LogUtils.logSevereMessage("No Regions in Appnames");
    }
  }

  @Test(
      expectedExceptions = StatusRuntimeException.class,
      expectedExceptionsMessageRegExp = "NOT_FOUND: Not found")
  @Description("GetByDeviceIdRequest - NOT_FOUND")
  @TmsLink("28500383")
  public void C28500383_TEST() {
    appNamesClient.getByDeviceIdRequest("testDal");
  }

  @Test
  @Description("GetAll - return all app names")
  @TmsLink("28500384")
  public void C28500384_TEST() {
    List<AppNames> getAllAppNameMongoDBList = appNamesUtils.getListOfAllObjects();
    LogUtils.logInfoMessage("Size of AppNames: " + getAllAppNameMongoDBList.size());
    List<tv.pluto.ip.dal.appname.AppNames.AppName> getAllAppNameList = appNamesClient.getAll();
    List<AppNames> grpcListAppNames =
        appNamesUtils.mapGrpcObjectsToPojo(getAllAppNameList, AppNames.class);

    getAllAppNameMongoDBList.sort(Comparator.comparing(AppNames::getId));
    grpcListAppNames.sort(Comparator.comparing(AppNames::getId));

    Assert.assertFalse(grpcListAppNames.isEmpty());
    Assert.assertEquals(getAllAppNameMongoDBList, grpcListAppNames);
  }

  @Test(dataProvider = "sort-values-true-false")
  @Description("GetAllPaged - return list of device objects")
  @TmsLink("28500386")
  public void C28500386_TEST(int sort, boolean totalCount) {
    List<AppNames> listDevices = appNamesUtils.getDocuments(999_999);
    int limit = 10;
    int offset = 0;
    LogUtils.logInfoMessage("Limit: " + limit + ", offset: " + offset);

    List<tv.pluto.ip.dal.appname.AppNames.GetAllPagedSort> grpcSort =
        Collections.singletonList(
            tv.pluto.ip.dal.appname.AppNames.GetAllPagedSort.newBuilder()
                .setField("name")
                .setValue(sort)
                .build());

    GetAllPagedResponse responseGetAllPaged =
        appNamesClient.getAllPaged(limit, offset, grpcSort, null, totalCount);

    if (totalCount) Assert.assertEquals(limit, responseGetAllPaged.getMetadata().getTotalItems());
    else Assert.assertEquals(listDevices.size(), responseGetAllPaged.getMetadata().getTotalItems());

    Assert.assertEquals(limit, responseGetAllPaged.getDocumentsList().size());
    Assert.assertEquals(offset, responseGetAllPaged.getMetadata().getCurrentOffset());

    if (sort == 1) listDevices.sort(Comparator.comparing(AppNames::getName));
    else listDevices.sort((a, b) -> b.getName().compareTo(a.getName()));

    List<AppNames> subListDevicesAll = listDevices.subList(offset, listDevices.size() - 1);
    List<AppNames> subListDevices = subListDevicesAll.subList(0, limit);

    List<AppNames> listGrpcDevices =
        appNamesUtils.mapGrpcObjectsToPojo(responseGetAllPaged.getDocumentsList(), AppNames.class);
    Assert.assertEquals(subListDevices, listGrpcDevices);
  }

  @Test
  @Description("Create - create an app with an authentication token")
  @TmsLink("28500389")
  public void C28500389_TEST() {
    Instant timeBeforeTest = Instant.now();
    String newName = "DAL" + UUID.randomUUID();
    LogUtils.logInfoMessage("Name: " + newName);
    tv.pluto.ip.dal.appname.AppNames.AppName appName =
        tv.pluto.ip.dal.appname.AppNames.AppName.newBuilder().setName(newName).build();

    tv.pluto.ip.dal.appname.AppNames.CreateRequest request =
        tv.pluto.ip.dal.appname.AppNames.CreateRequest.newBuilder().setDocument(appName).build();

    AppName responseApp = appNamesClientAuth.create(request).getDocument();
    Assert.assertNotNull(responseApp.getId());
    appNameList.add(responseApp);

    Assert.assertEquals(responseApp.getName(), newName);
    Instant createdTime = Instant.parse(responseApp.getCreatedAt());
    Assert.assertTrue(timeBeforeTest.isBefore(createdTime));
  }

  @Test(
      expectedExceptions = RuntimeException.class,
      expectedExceptionsMessageRegExp =
          "UNAUTHENTICATED: DB writes not allowed without authentication")
  @Description("Create - create an app without an authentication token")
  @TmsLink("28500390")
  public void C28500390_TEST() {
    String newName = "DAL" + UUID.randomUUID();
    LogUtils.logInfoMessage("Name: " + newName);
    tv.pluto.ip.dal.appname.AppNames.AppName appName =
        tv.pluto.ip.dal.appname.AppNames.AppName.newBuilder().setName(newName).build();

    tv.pluto.ip.dal.appname.AppNames.CreateRequest request =
        tv.pluto.ip.dal.appname.AppNames.CreateRequest.newBuilder().setDocument(appName).build();

    appNamesClient.create(request).getDocument();
  }

  @Test(dependsOnMethods = "C28500389_TEST")
  @Description("UpdateFields - update all fields")
  @TmsLink("28500391")
  public void C28500391_TEST() {
    String newName = "DAL" + UUID.randomUUID();
    LogUtils.logInfoMessage("New name: " + newName);

    tv.pluto.ip.dal.appname.AppNames.AppName appName = appNameList.get(0);
    tv.pluto.ip.dal.appname.AppNames.AppName newAppName =
        tv.pluto.ip.dal.appname.AppNames.AppName.newBuilder().setName(newName).build();

    tv.pluto.ip.dal.appname.AppNames.UpdateFieldsRequest request =
        tv.pluto.ip.dal.appname.AppNames.UpdateFieldsRequest.newBuilder()
            .setId(appName.getId())
            .setDocument(newAppName)
            .addAllUpdateFields(List.of("name"))
            .build();

    AppName updatedApp = appNamesClient.update(request).getDocument();
    Assert.assertEquals(updatedApp.getId(), appName.getId());
    Assert.assertEquals(updatedApp.getDevicesList(), appName.getDevicesList());
    Assert.assertEquals(updatedApp.getDevicesrevList(), appName.getDevicesrevList());
    Assert.assertEquals(updatedApp.getRegionsList(), appName.getRegionsList());
    Assert.assertEquals(updatedApp.getCreatedAt(), appName.getCreatedAt());
    Assert.assertEquals(updatedApp.getName(), newName);
  }

  @Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "INTERNAL.*")
  @Description("UpdateFields - INTERNAL error")
  @TmsLink("28500392")
  public void C28500392_TEST() {
    tv.pluto.ip.dal.appname.AppNames.UpdateFieldsRequest request =
        tv.pluto.ip.dal.appname.AppNames.UpdateFieldsRequest.newBuilder()
            .setId("test")
            .addAllUpdateFields(List.of("name"))
            .build();

    appNamesClient.update(request);
  }
}
