/**
 * Copyright © 2016-2026 The Thingsboard Authors
 *
 * Licensed 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.thingsboard.server.edge;

import com.fasterxml.jackson.databind.node.TextNode;
import com.google.protobuf.AbstractMessage;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.RelationRequestMsg;
import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;

import java.util.UUID;

@DaoSqlTest
public class RelationEdgeTest extends AbstractEdgeTest {


    @Test
    public void testRelations() throws Exception {
        // create relation
        Device device = findDeviceByName("Edge Device 1");
        Asset asset = findAssetByName("Edge Asset 1");
        EntityRelation relation = new EntityRelation();
        relation.setType("test");
        relation.setFrom(device.getId());
        relation.setTo(asset.getId());
        relation.setTypeGroup(RelationTypeGroup.COMMON);
        edgeImitator.expectMessageAmount(1);
        relation = doPost("/api/v2/relation", relation, EntityRelation.class);
        Assert.assertTrue(edgeImitator.waitForMessages());
        AbstractMessage latestMessage = edgeImitator.getLatestMessage();
        Assert.assertTrue(latestMessage instanceof RelationUpdateMsg);
        RelationUpdateMsg relationUpdateMsg = (RelationUpdateMsg) latestMessage;
        EntityRelation entityRelation = JacksonUtil.fromString(relationUpdateMsg.getEntity(), EntityRelation.class, true);
        Assert.assertNotNull(entityRelation);
        compareHasVersionEntities(relation, entityRelation);
        Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, relationUpdateMsg.getMsgType());

        // delete relation
        edgeImitator.expectMessageAmount(1);

        String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&relationTypeGroup=%s&toId=%s&toType=%s",
                device.getId().toString(), EntityType.DEVICE.name(), "test",
                RelationTypeGroup.COMMON.name(), asset.getId().toString(), EntityType.ASSET.name()
        );

        var deletedRelation = doDelete(deleteUrl, EntityRelation.class);

        Assert.assertTrue(edgeImitator.waitForMessages());
        latestMessage = edgeImitator.getLatestMessage();
        Assert.assertTrue(latestMessage instanceof RelationUpdateMsg);
        relationUpdateMsg = (RelationUpdateMsg) latestMessage;
        entityRelation = JacksonUtil.fromString(relationUpdateMsg.getEntity(), EntityRelation.class, true);
        Assert.assertNotNull(entityRelation);
        compareHasVersionEntities(deletedRelation, entityRelation);
        Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, relationUpdateMsg.getMsgType());
    }

    @Test
    public void testSendRelationToCloud() throws Exception {
        Device device1 = saveDeviceOnCloudAndVerifyDeliveryToEdge();
        Device device2 = saveDeviceOnCloudAndVerifyDeliveryToEdge();

        EntityRelation entityRelation = buildEntityRelationForUplinkMsg(device1.getId(), device2.getId());

        UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
        RelationUpdateMsg.Builder relationUpdateMsgBuilder = RelationUpdateMsg.newBuilder();
        relationUpdateMsgBuilder.setEntity(JacksonUtil.toString(entityRelation));
        testAutoGeneratedCodeByProtobuf(relationUpdateMsgBuilder);
        uplinkMsgBuilder.addRelationUpdateMsg(relationUpdateMsgBuilder.build());

        testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);

        edgeImitator.expectResponsesAmount(1);
        edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
        Assert.assertTrue(edgeImitator.waitForResponses());

        String getUrl = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&relationTypeGroup=%s&toId=%s&toType=%s",
                device2.getUuidId(), EntityType.DEVICE.name(), "test",
                RelationTypeGroup.COMMON.name(), device1.getUuidId(), EntityType.DEVICE.name()
        );

        var relation = doGet(getUrl, EntityRelation.class);

        Assert.assertNotNull(relation);
    }

    @Test
    public void testSendRelationRequestToCloud() throws Exception {
        Device device = findDeviceByName("Edge Device 1");
        Asset asset = findAssetByName("Edge Asset 1");

        EntityRelation deviceToAssetRelation = new EntityRelation();
        deviceToAssetRelation.setType("test");
        deviceToAssetRelation.setFrom(device.getId());
        deviceToAssetRelation.setTo(asset.getId());
        deviceToAssetRelation.setTypeGroup(RelationTypeGroup.COMMON);

        edgeImitator.expectMessageAmount(1);
        deviceToAssetRelation = doPost("/api/v2/relation", deviceToAssetRelation, EntityRelation.class);
        Assert.assertTrue(edgeImitator.waitForMessages());

        EntityRelation assetToTenantRelation = new EntityRelation();
        assetToTenantRelation.setType("test");
        assetToTenantRelation.setFrom(asset.getId());
        assetToTenantRelation.setTo(tenantId);
        assetToTenantRelation.setTypeGroup(RelationTypeGroup.COMMON);

        edgeImitator.expectMessageAmount(1);
        doPost("/api/relation", assetToTenantRelation);
        Assert.assertTrue(edgeImitator.waitForMessages());

        UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
        RelationRequestMsg.Builder relationRequestMsgBuilder = RelationRequestMsg.newBuilder();
        relationRequestMsgBuilder.setEntityIdMSB(device.getId().getId().getMostSignificantBits());
        relationRequestMsgBuilder.setEntityIdLSB(device.getId().getId().getLeastSignificantBits());
        relationRequestMsgBuilder.setEntityType(device.getId().getEntityType().name());
        testAutoGeneratedCodeByProtobuf(relationRequestMsgBuilder);

        uplinkMsgBuilder.addRelationRequestMsg(relationRequestMsgBuilder.build());
        testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);

        edgeImitator.expectResponsesAmount(1);
        edgeImitator.expectMessageAmount(1);
        edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
        Assert.assertTrue(edgeImitator.waitForResponses());
        Assert.assertTrue(edgeImitator.waitForMessages());

        AbstractMessage latestMessage = edgeImitator.getLatestMessage();
        Assert.assertTrue(latestMessage instanceof RelationUpdateMsg);
        RelationUpdateMsg relationUpdateMsg = (RelationUpdateMsg) latestMessage;
        EntityRelation entityRelation = JacksonUtil.fromString(relationUpdateMsg.getEntity(), EntityRelation.class, true);
        Assert.assertNotNull(entityRelation);
        compareHasVersionEntities(deviceToAssetRelation, entityRelation);
        Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, relationUpdateMsg.getMsgType());
    }

    @Test
    public void testRelationFromEdgeToDevice() throws Exception {
        // create relation
        Device device = findDeviceByName("Edge Device 1");
        EdgeId edgeId = new EdgeId(new UUID(edgeImitator.getConfiguration().getEdgeIdMSB(), edgeImitator.getConfiguration().getEdgeIdLSB()));
        EntityRelation relation = new EntityRelation();
        relation.setType("test");
        relation.setFrom(edgeId);
        relation.setTo(device.getId());
        relation.setTypeGroup(RelationTypeGroup.COMMON);
        edgeImitator.expectMessageAmount(1);
        relation = doPost("/api/v2/relation", relation, EntityRelation.class);
        Assert.assertTrue(edgeImitator.waitForMessages());
        AbstractMessage latestMessage = edgeImitator.getLatestMessage();
        Assert.assertTrue(latestMessage instanceof RelationUpdateMsg);
        RelationUpdateMsg relationUpdateMsg = (RelationUpdateMsg) latestMessage;
        EntityRelation entityRelation = JacksonUtil.fromString(relationUpdateMsg.getEntity(), EntityRelation.class, true);
        Assert.assertNotNull(entityRelation);
        compareHasVersionEntities(relation, entityRelation);
        Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, relationUpdateMsg.getMsgType());

        // delete relation
        edgeImitator.expectMessageAmount(1);

        String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&relationTypeGroup=%s&toId=%s&toType=%s",
                edgeId, EntityType.EDGE.name(), "test",
                RelationTypeGroup.COMMON.name(), device.getId().toString(), EntityType.DEVICE.name()
        );

        var deletedRelation = doDelete(deleteUrl, EntityRelation.class);

        Assert.assertTrue(edgeImitator.waitForMessages());
        latestMessage = edgeImitator.getLatestMessage();
        Assert.assertTrue(latestMessage instanceof RelationUpdateMsg);
        relationUpdateMsg = (RelationUpdateMsg) latestMessage;
        entityRelation = JacksonUtil.fromString(relationUpdateMsg.getEntity(), EntityRelation.class, true);
        Assert.assertNotNull(entityRelation);
        compareHasVersionEntities(deletedRelation, entityRelation);
        Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, relationUpdateMsg.getMsgType());
    }

    private EntityRelation buildEntityRelationForUplinkMsg(DeviceId deviceId1, DeviceId deviceId2) {
        EntityRelation relation = new EntityRelation();
        relation.setType("test");
        relation.setTypeGroup(RelationTypeGroup.COMMON);
        relation.setTo(deviceId1);
        relation.setFrom(deviceId2);
        relation.setAdditionalInfo(TextNode.valueOf("{}"));
        return relation;
    }

}
