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

import static com.mongodb.client.model.Projections.include;
import static tv.pluto.ip.dal.device.Devices.Device;

import com.plutotv.test.lib.LogUtils;
import io.grpc.StatusRuntimeException;
import io.qameta.allure.Description;
import io.qameta.allure.TmsLink;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
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.devices.DevicesClient;
import tv.pluto.ip.dal.device.Devices.GetAllPagedResponse;
import tv.pluto.mongo.dto.ContentFeedQuery;
import tv.pluto.mongo.entities.devices.Devices;
import tv.pluto.mongo.utils.DevicesUtils;

public class DeviceTestSuite extends BaseTest {
  private DevicesClient devicesClient;
  private DevicesUtils devicesUtils;
  private List<Device> deviceList;

  @BeforeClass(alwaysRun = true)
  private void setup() {
    devicesClient = new DevicesClient(grpcChannelManager.getChannel(), false);
    devicesUtils = new DevicesUtils();
    deviceList = new ArrayList<>();
  }

  @Test
  @Description("GetDocumentsUpdatedBetweenDates - Time period for which there is a device")
  @TmsLink("28500761")
  public void C28500761_TEST() {

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

    List<Devices> mongoDevices = devicesUtils.getDocumentsForContentFeeds(contentFeedQuery);
    List<Devices> grpcDevices = new ArrayList<>();

    Iterator<Device> devices =
        devicesClient.getDocumentsUpdatedBetweenDates(
            devicesUtils.updatedAtMinString, devicesUtils.updatedAtMaxString);

    while (devices.hasNext()) {
      grpcDevices.add(devicesUtils.mapGrpcObjectToPojo(devices.next(), Devices.class));
    }

    mongoDevices.sort(Comparator.comparing(Devices::getId));
    grpcDevices.sort(Comparator.comparing(Devices::getId));

    List<String> mongoIds = mongoDevices.stream().map(Devices::getId).toList();
    List<String> grpcIds = grpcDevices.stream().map(Devices::getId).toList();

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

  @Test(
      expectedExceptions = StatusRuntimeException.class,
      expectedExceptionsMessageRegExp = NOT_FOUND_MESSAGE)
  @Description("GetDocumentsUpdatedBetweenDates - Time period without devices")
  @TmsLink("28500750")
  public void C28500750_TEST() {
    devicesClient.getDocumentsUpdatedBetweenDates(failStartDateTime, failEndDateTime).next();
  }

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

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

  @Test
  @Description("GetById - getById")
  @TmsLink("28500762")
  public void C28500762_TEST() {
    Devices mangoDevice = devicesUtils.getRandomDocument();
    tv.pluto.ip.dal.device.Devices.GetByIdResponse grpcDeviceResponse =
        devicesClient.getById(mangoDevice.getId());
    Devices grpDevice =
        devicesUtils.mapGrpcObjectToPojo(grpcDeviceResponse.getDocument(), Devices.class);
    Assert.assertEquals(mangoDevice, grpDevice);
  }

  @Test(
      expectedExceptions = StatusRuntimeException.class,
      expectedExceptionsMessageRegExp = "NOT_FOUND: Device document for requested ID not found")
  @Description("GetById - getById error handling with invalid id")
  @TmsLink("28500763")
  public void C28500763_TEST() {
    devicesClient.getById("582b7a7df12f0f02f1999999");
  }

  @Test(dataProvider = "sort-values-true-false")
  @TmsLink("28500764")
  @Description("GetAllPaged - return list of device objects")
  public void C28500764_TEST(int sort, boolean totalCount) {
    List<Devices> listDevices = devicesUtils.getDocuments(999_999);
    int limit = new Random().nextInt(listDevices.size()) / 2 + 1;
    int offset = limit / 2;
    LogUtils.logInfoMessage("Limit: " + limit + ", offset: " + offset);

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

    GetAllPagedResponse responseGetAllPaged =
        new DevicesClient(grpcChannelManager.getChannel(), false)
            .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());

    List<Devices> subListDevicesAll = listDevices.subList(offset, listDevices.size() - 1);
    List<Devices> subListDevices = subListDevicesAll.subList(0, limit);
    Assert.assertEquals(subListDevices.size(), responseGetAllPaged.getDocumentsList().size());
  }

  @Test(
      expectedExceptions = RuntimeException.class,
      expectedExceptionsMessageRegExp = INTERNAL_MESSAGE)
  @TmsLink("28500765")
  @Description("GetAllPaged - object id in filter is not correct. INTERNAL exception")
  public void C28500765_TEST()
      throws NoSuchFieldException, InvocationTargetException, IllegalAccessException {
    // triggers INTERNAL exception
    tv.pluto.ip.dal.device.Devices.Filter filter =
        tv.pluto.ip.dal.device.Devices.Filter.newBuilder()
            .setField("_id")
            .setValue("TEST")
            .setType("string")
            .build();

    new DevicesClient(grpcChannelManager.getChannel(), false)
        .getAllPagedException(null, null, null, List.of(filter), null);
  }

  @Test
  @Description("GetById has a partner array")
  @TmsLink("28500768")
  public void C28500768_TEST() {
    try {
      Devices device = devicesUtils.getRandomDocumentWithTheField("partner", true);
      Device deviceGrpc = devicesClient.getById(device.getId()).getDocument();
      Devices devicesGrpc = devicesUtils.mapGrpcObjectToPojo(deviceGrpc, Devices.class);

      Assert.assertFalse(device.getId().isEmpty());
      Assert.assertNotNull(deviceGrpc.getPartnersList());
      Assert.assertEquals(devicesGrpc, device);
    } catch (NullPointerException e) {
      LogUtils.logSevereMessage("No data in devices for partner!!!");
    }
  }

  @Test
  @Description("GetById has a revplatform field")
  @TmsLink("28500769")
  public void C28500769_TEST() {
    try {
      Devices device = devicesUtils.getRandomDocumentWithTheField("revplatform", true);
      Device deviceGrpc = devicesClient.getById(device.getId()).getDocument();
      Devices gDevice = devicesUtils.mapGrpcObjectToPojo(deviceGrpc, Devices.class);

      Assert.assertFalse(gDevice.getRevplatform().isEmpty());
      Assert.assertEquals(gDevice, device);
    } catch (NullPointerException e) {
      LogUtils.logSevereMessage("No data in devices for revplatform!!!");
    }
  }

  @Test
  @Description("Create - a new device with an authentication token")
  @TmsLink("28500770")
  public void C28500770_TEST() {
    String newName = "DAL" + UUID.randomUUID();
    GetAllPagedResponse responseGetAllPaged =
        new DevicesClient(grpcChannelManager.getChannel(), false)
            .getAllPaged(1, 0, null, null, true);
    Device device = responseGetAllPaged.getDocumentsList().get(0);
    Device deviceNewName = device.toBuilder().setName(newName).setId("").build();
    tv.pluto.ip.dal.device.Devices.CreateRequest request =
        tv.pluto.ip.dal.device.Devices.CreateRequest.newBuilder()
            .setDocument(deviceNewName)
            .build();

    Device responseDevice =
        new DevicesClient(grpcChannelManager.getChannel(), true).create(request).getDocument();
    LogUtils.logInfoMessage("Created a new device id: " + responseDevice.getId());

    Assert.assertEquals(responseDevice.getPartnersList(), device.getPartnersList());
    Assert.assertEquals(responseDevice.getRevplatform(), device.getRevplatform());
    Assert.assertEquals(responseDevice.getDisabled(), device.getDisabled());
    Assert.assertEquals(responseDevice.getSlug(), newName.toLowerCase());
    Assert.assertEquals(responseDevice.getName(), newName);
    deviceList.add(responseDevice);
  }

  @Test(
      expectedExceptions = RuntimeException.class,
      expectedExceptionsMessageRegExp =
          "UNAUTHENTICATED: DB writes not allowed without authentication")
  @Description("Create - a new device without authentication token")
  @TmsLink("28500771")
  public void C28500771_TEST() {
    GetAllPagedResponse responseGetAllPaged =
        new DevicesClient(grpcChannelManager.getChannel(), false)
            .getAllPaged(1, 0, null, null, true);

    Device device = responseGetAllPaged.getDocumentsList().get(0);
    Device deviceNewName = device.toBuilder().setName(UUID.randomUUID().toString()).build();
    tv.pluto.ip.dal.device.Devices.CreateRequest request =
        tv.pluto.ip.dal.device.Devices.CreateRequest.newBuilder()
            .setDocument(deviceNewName)
            .build();
    devicesClient.create(request).getDocument();
  }

  @Test(dependsOnMethods = "C28500770_TEST")
  @Description("UpdateFields - name field")
  @TmsLink("28500772")
  public void C28500772_TEST() {
    Device testedDevice = deviceList.get(0);
    String newName = "DAL" + UUID.randomUUID();
    Device device = Device.newBuilder().setName(newName).build();

    tv.pluto.ip.dal.device.Devices.UpdateFieldsRequest request =
        tv.pluto.ip.dal.device.Devices.UpdateFieldsRequest.newBuilder()
            .setDocument(device)
            .setId(testedDevice.getId())
            .addAllUpdateFields(List.of("name"))
            .build();

    Device responseDevice = devicesClient.update(request).getDocument();

    Assert.assertEquals(responseDevice.getId(), testedDevice.getId());
    Assert.assertEquals(responseDevice.getManufacturer(), testedDevice.getManufacturer());
    Assert.assertEquals(responseDevice.getPlatform(), testedDevice.getPlatform());
    Assert.assertEquals(responseDevice.getSlug(), testedDevice.getSlug());
    Assert.assertEquals(responseDevice.getCreatedAt(), testedDevice.getCreatedAt());
    Assert.assertEquals(responseDevice.getCreatedAt(), testedDevice.getCreatedAt());
    Assert.assertEquals(responseDevice.getName(), newName);
  }

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

    devicesClient.update(request);
  }

  @Test
  @Description("Create - a new device with the partners array")
  @TmsLink("28500775")
  public void C28500775_TEST() {
    String newName = "DAL" + UUID.randomUUID();
    GetAllPagedResponse responseGetAllPaged =
        new DevicesClient(grpcChannelManager.getChannel(), false)
            .getAllPaged(1, 0, null, null, true);
    Device device = responseGetAllPaged.getDocumentsList().get(0);

    tv.pluto.ip.dal.device.Devices.Partner partnerOne =
        tv.pluto.ip.dal.device.Devices.Partner.newBuilder()
            .setName("name1")
            .addAllRegions(List.of("us", "es"))
            .build();

    tv.pluto.ip.dal.device.Devices.Partner partnerTwo =
        tv.pluto.ip.dal.device.Devices.Partner.newBuilder()
            .setName("name2")
            .addAllRegions(List.of("en"))
            .build();

    List<tv.pluto.ip.dal.device.Devices.Partner> listDevices = List.of(partnerOne, partnerTwo);

    Device newDevice =
        device.toBuilder().setName(newName).addAllPartners(listDevices).setId("").build();
    tv.pluto.ip.dal.device.Devices.CreateRequest request =
        tv.pluto.ip.dal.device.Devices.CreateRequest.newBuilder().setDocument(newDevice).build();

    Device responseDevice =
        new DevicesClient(grpcChannelManager.getChannel(), true).create(request).getDocument();
    LogUtils.logInfoMessage("Created a new device id: " + responseDevice.getId());

    Assert.assertEquals(
        new HashSet<>(responseDevice.getPartnersList()).size(), new HashSet<>(listDevices).size());

    Assert.assertEquals(
        new HashSet<>(responseDevice.getPartnersList()), new HashSet<>(listDevices));

    Assert.assertEquals(responseDevice.getRevplatform(), device.getRevplatform());
    Assert.assertEquals(responseDevice.getDisabled(), device.getDisabled());
    Assert.assertEquals(responseDevice.getSlug(), newName.toLowerCase());
    Assert.assertEquals(responseDevice.getName(), newName);
    deviceList.add(responseDevice);
  }

  @Test
  @Description("UpdateFields - partner array")
  @TmsLink("28500776")
  public void C28500776_TEST() {
    GetAllPagedResponse responseGetAllPaged =
        new DevicesClient(grpcChannelManager.getChannel(), false)
            .getAllPaged(2, 1, null, null, true);
    Device testedDevice = responseGetAllPaged.getDocumentsList().get(0);
    String newName = "DAL" + UUID.randomUUID();

    tv.pluto.ip.dal.device.Devices.Partner partnerOne =
        tv.pluto.ip.dal.device.Devices.Partner.newBuilder()
            .setName("name1")
            .addAllRegions(List.of("us", "es"))
            .build();

    tv.pluto.ip.dal.device.Devices.Partner partnerTwo =
        tv.pluto.ip.dal.device.Devices.Partner.newBuilder()
            .setName("name2")
            .addAllRegions(List.of("en"))
            .build();

    List<tv.pluto.ip.dal.device.Devices.Partner> listDevices = List.of(partnerOne, partnerTwo);
    Device newDevice =
        testedDevice.toBuilder().setName(newName).addAllPartners(listDevices).build();

    tv.pluto.ip.dal.device.Devices.UpdateFieldsRequest request =
        tv.pluto.ip.dal.device.Devices.UpdateFieldsRequest.newBuilder()
            .setDocument(newDevice)
            .setId(testedDevice.getId())
            .addAllUpdateFields(List.of("name", "partners"))
            .build();

    Device responseDevice = devicesClient.update(request).getDocument();

    Assert.assertTrue(responseDevice.getPartnersList().size() >= 2);
    Assert.assertTrue(responseDevice.getPartnersList().contains(partnerOne));
    Assert.assertTrue(responseDevice.getPartnersList().contains(partnerTwo));
    Assert.assertTrue(responseDevice.getPartnersList().containsAll(listDevices));

    Assert.assertEquals(responseDevice.getId(), testedDevice.getId());
    Assert.assertEquals(responseDevice.getManufacturer(), testedDevice.getManufacturer());
    Assert.assertEquals(responseDevice.getPlatform(), testedDevice.getPlatform());
    Assert.assertEquals(responseDevice.getSlug(), testedDevice.getSlug());
    Assert.assertEquals(responseDevice.getCreatedAt(), testedDevice.getCreatedAt());
    Assert.assertEquals(responseDevice.getCreatedAt(), testedDevice.getCreatedAt());
    Assert.assertEquals(responseDevice.getName(), newName);
  }

  @Test(
      expectedExceptions = RuntimeException.class,
      expectedExceptionsMessageRegExp =
          "UNAUTHENTICATED: app_name must be provided in request metadata")
  @Description("UpdateFields - name field no Metadata")
  @TmsLink("28500774")
  public void C28500774_TEST() {
    GetAllPagedResponse responseGetAllPaged =
        new DevicesClient(grpcChannelManager.getChannel(), false)
            .getAllPaged(3, 2, null, null, true);
    Device testedDevice = responseGetAllPaged.getDocumentsList().get(0);
    String newName = "DAL" + UUID.randomUUID();

    Device newDevice = testedDevice.toBuilder().setName(newName).build();

    tv.pluto.ip.dal.device.Devices.UpdateFieldsRequest request =
        tv.pluto.ip.dal.device.Devices.UpdateFieldsRequest.newBuilder()
            .setDocument(newDevice)
            .setId(testedDevice.getId())
            .addAllUpdateFields(List.of("name"))
            .build();

    new DevicesClient(grpcChannelManager.getChannel()).updateNoMeta(request);
  }
}
