/*
 * 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.gravitino.storage.relational.service;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Comparator;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.authorization.AuthorizationUtils;
import org.apache.gravitino.authorization.Privileges;
import org.apache.gravitino.authorization.SecurableObject;
import org.apache.gravitino.authorization.SecurableObjects;
import org.apache.gravitino.meta.BaseMetalake;
import org.apache.gravitino.meta.CatalogEntity;
import org.apache.gravitino.meta.FilesetEntity;
import org.apache.gravitino.meta.FunctionEntity;
import org.apache.gravitino.meta.ModelEntity;
import org.apache.gravitino.meta.RoleEntity;
import org.apache.gravitino.meta.SchemaEntity;
import org.apache.gravitino.meta.TableEntity;
import org.apache.gravitino.meta.TopicEntity;
import org.apache.gravitino.meta.ViewEntity;
import org.apache.gravitino.storage.RandomIdGenerator;
import org.apache.gravitino.storage.relational.TestJDBCBackend;
import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
import org.apache.ibatis.session.SqlSession;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.TestTemplate;

public class TestSecurableObjects extends TestJDBCBackend {
  private final RoleMetaService roleMetaService = RoleMetaService.getInstance();

  @TestTemplate
  public void testAllTypeSecurableObjects() throws IOException {
    String metalakeName = "metalake";
    BaseMetalake metalake = createAndInsertMakeLake(metalakeName);

    CatalogEntity catalog =
        createCatalog(
            RandomIdGenerator.INSTANCE.nextId(), Namespace.of(metalakeName), "catalog", AUDIT_INFO);
    backend.insert(catalog, false);

    CatalogEntity catalog2 =
        createCatalog(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake"),
            metalake.name(),
            AUDIT_INFO);
    backend.insert(catalog2, false);

    SchemaEntity schema =
        createSchemaEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of(metalakeName, "catalog"),
            "schema",
            AUDIT_INFO);
    backend.insert(schema, false);

    FilesetEntity fileset =
        createFilesetEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of(metalakeName, "catalog", "schema"),
            "fileset",
            AUDIT_INFO);
    backend.insert(fileset, false);

    TableEntity table =
        createTableEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of(metalakeName, "catalog", "schema"),
            "table",
            AUDIT_INFO);
    backend.insert(table, false);

    TopicEntity topic =
        createTopicEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of(metalakeName, "catalog", "schema"),
            "topic",
            AUDIT_INFO);
    backend.insert(topic, false);

    ViewEntity view =
        createViewEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of(metalakeName, "catalog", "schema"),
            "view");
    backend.insert(view, false);

    SecurableObject metalakeObject =
        SecurableObjects.ofMetalake(
            metalake.name(), Lists.newArrayList(Privileges.UseCatalog.allow()));

    SecurableObject catalogObject =
        SecurableObjects.ofCatalog(
            "catalog",
            Lists.newArrayList(Privileges.UseCatalog.allow(), Privileges.CreateSchema.deny()));

    SecurableObject catalogObject2 =
        SecurableObjects.ofCatalog(
            metalake.name(), Lists.newArrayList(Privileges.UseCatalog.allow()));

    SecurableObject schemaObject =
        SecurableObjects.ofSchema(
            catalogObject, "schema", Lists.newArrayList(Privileges.UseSchema.allow()));

    SecurableObject tableObject =
        SecurableObjects.ofTable(
            schemaObject, "table", Lists.newArrayList(Privileges.SelectTable.allow()));

    SecurableObject filesetObject =
        SecurableObjects.ofFileset(
            schemaObject, "fileset", Lists.newArrayList(Privileges.ReadFileset.allow()));

    SecurableObject topicObject =
        SecurableObjects.ofTopic(
            schemaObject, "topic", Lists.newArrayList(Privileges.ConsumeTopic.deny()));

    SecurableObject viewObject =
        SecurableObjects.ofView(
            schemaObject, "view", Lists.newArrayList(Privileges.SelectTable.allow()));

    ArrayList<SecurableObject> securableObjects =
        Lists.newArrayList(
            metalakeObject,
            catalogObject2,
            catalogObject,
            schemaObject,
            tableObject,
            filesetObject,
            topicObject,
            viewObject);
    securableObjects.sort(Comparator.comparing(MetadataObject::fullName));

    RoleEntity role1 =
        createRoleEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            AuthorizationUtils.ofRoleNamespace(metalakeName),
            "role1",
            AUDIT_INFO,
            securableObjects,
            ImmutableMap.of("k1", "v1"));

    Assertions.assertDoesNotThrow(() -> roleMetaService.insertRole(role1, false));
    Assertions.assertEquals(role1, roleMetaService.getRoleByIdentifier(role1.nameIdentifier()));
  }

  @TestTemplate
  public void testDeleteMetadataObject() throws IOException {
    String metalakeName = "metalake";
    BaseMetalake metalake =
        createBaseMakeLake(RandomIdGenerator.INSTANCE.nextId(), metalakeName, AUDIT_INFO);
    backend.insert(metalake, false);

    CatalogEntity catalog =
        createCatalog(
            RandomIdGenerator.INSTANCE.nextId(), Namespace.of("metalake"), "catalog", AUDIT_INFO);
    backend.insert(catalog, false);

    SchemaEntity schema =
        createSchemaEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog"),
            "schema",
            AUDIT_INFO);
    backend.insert(schema, false);

    FilesetEntity fileset =
        createFilesetEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog", "schema"),
            "fileset",
            AUDIT_INFO);
    backend.insert(fileset, false);
    TableEntity table =
        createTableEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog", "schema"),
            "table",
            AUDIT_INFO);
    backend.insert(table, false);
    TopicEntity topic =
        createTopicEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog", "schema"),
            "topic",
            AUDIT_INFO);
    backend.insert(topic, false);
    ModelEntity model =
        createModelEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog", "schema"),
            "model",
            "comment",
            1,
            null,
            AUDIT_INFO);
    backend.insert(model, false);

    ViewEntity view =
        createViewEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog", "schema"),
            "view");
    backend.insert(view, false);
    FunctionEntity function =
        createFunctionEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog", "schema"),
            "function",
            AUDIT_INFO);
    backend.insert(function, false);

    SecurableObject catalogObject =
        SecurableObjects.ofCatalog(
            "catalog",
            Lists.newArrayList(Privileges.UseCatalog.allow(), Privileges.CreateSchema.deny()));

    SecurableObject schemaObject =
        SecurableObjects.ofSchema(
            catalogObject, "schema", Lists.newArrayList(Privileges.UseSchema.allow()));
    SecurableObject tableObject =
        SecurableObjects.ofTable(
            schemaObject, "table", Lists.newArrayList(Privileges.SelectTable.allow()));
    SecurableObject filesetObject =
        SecurableObjects.ofFileset(
            schemaObject, "fileset", Lists.newArrayList(Privileges.ReadFileset.allow()));
    SecurableObject topicObject =
        SecurableObjects.ofTopic(
            schemaObject, "topic", Lists.newArrayList(Privileges.ConsumeTopic.deny()));
    SecurableObject modelObject =
        SecurableObjects.ofModel(
            schemaObject, "model", Lists.newArrayList(Privileges.UseModel.deny()));
    SecurableObject viewObject =
        SecurableObjects.ofView(
            schemaObject, "view", Lists.newArrayList(Privileges.SelectTable.allow()));
    SecurableObject functionObject =
        SecurableObjects.ofFunction(
            schemaObject, "function", Lists.newArrayList(Privileges.ExecuteFunction.allow()));

    RoleEntity role1 =
        createRoleEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            AuthorizationUtils.ofRoleNamespace(metalakeName),
            "role1",
            AUDIT_INFO,
            Lists.newArrayList(
                catalogObject,
                schemaObject,
                tableObject,
                filesetObject,
                topicObject,
                modelObject,
                viewObject,
                functionObject),
            ImmutableMap.of("k1", "v1"));

    roleMetaService.insertRole(role1, false);

    Assertions.assertEquals(8, countAllObjectRel(role1.id()));
    Assertions.assertEquals(8, countActiveObjectRel(role1.id()));

    // Test to delete view
    ViewMetaService.getInstance()
        .deleteView(NameIdentifier.of("metalake", "catalog", "schema", "view"));
    Assertions.assertEquals(8, countAllObjectRel(role1.id()));
    Assertions.assertEquals(7, countActiveObjectRel(role1.id()));

    // Test to delete model
    ModelMetaService.getInstance().deleteModel(model.nameIdentifier());
    Assertions.assertEquals(8, countAllObjectRel(role1.id()));
    Assertions.assertEquals(6, countActiveObjectRel(role1.id()));

    // Test to delete table
    TableMetaService.getInstance().deleteTable(table.nameIdentifier());
    Assertions.assertEquals(8, countAllObjectRel(role1.id()));
    Assertions.assertEquals(5, countActiveObjectRel(role1.id()));

    // Test to delete topic
    TopicMetaService.getInstance().deleteTopic(topic.nameIdentifier());
    Assertions.assertEquals(8, countAllObjectRel(role1.id()));
    Assertions.assertEquals(4, countActiveObjectRel(role1.id()));

    // Test to delete fileset
    FilesetMetaService.getInstance().deleteFileset(fileset.nameIdentifier());
    Assertions.assertEquals(8, countAllObjectRel(role1.id()));
    Assertions.assertEquals(3, countActiveObjectRel(role1.id()));

    // Test to delete function
    FunctionMetaService.getInstance().deleteFunction(function.nameIdentifier());
    Assertions.assertEquals(8, countAllObjectRel(role1.id()));
    Assertions.assertEquals(2, countActiveObjectRel(role1.id()));

    // Test to delete schema
    SchemaMetaService.getInstance().deleteSchema(schema.nameIdentifier(), false);
    Assertions.assertEquals(8, countAllObjectRel(role1.id()));
    Assertions.assertEquals(1, countActiveObjectRel(role1.id()));

    // Test to delete catalog
    CatalogMetaService.getInstance().deleteCatalog(catalog.nameIdentifier(), false);
    Assertions.assertEquals(8, countAllObjectRel(role1.id()));
    Assertions.assertEquals(0, countActiveObjectRel(role1.id()));

    roleMetaService.deleteRole(role1.nameIdentifier());

    // Test to delete catalog with cascade mode
    catalog =
        createCatalog(
            RandomIdGenerator.INSTANCE.nextId(), Namespace.of("metalake"), "catalog", AUDIT_INFO);
    backend.insert(catalog, false);

    schema =
        createSchemaEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog"),
            "schema",
            AUDIT_INFO);
    backend.insert(schema, false);

    fileset =
        createFilesetEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog", "schema"),
            "fileset",
            AUDIT_INFO);
    backend.insert(fileset, false);
    table =
        createTableEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog", "schema"),
            "table",
            AUDIT_INFO);
    backend.insert(table, false);

    topic =
        createTopicEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog", "schema"),
            "topic",
            AUDIT_INFO);
    backend.insert(topic, false);

    model =
        createModelEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog", "schema"),
            "model",
            "comment",
            1,
            null,
            AUDIT_INFO);
    backend.insert(model, false);

    view =
        createViewEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog", "schema"),
            "view");
    backend.insert(view, false);
    FunctionEntity function2 =
        createFunctionEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog", "schema"),
            "function",
            AUDIT_INFO);
    backend.insert(function2, false);

    role1 =
        createRoleEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            AuthorizationUtils.ofRoleNamespace(metalakeName),
            "role1",
            AUDIT_INFO,
            Lists.newArrayList(
                catalogObject,
                schemaObject,
                tableObject,
                filesetObject,
                topicObject,
                modelObject,
                viewObject,
                functionObject),
            ImmutableMap.of("k1", "v1"));

    roleMetaService.insertRole(role1, false);

    CatalogMetaService.getInstance().deleteCatalog(catalog.nameIdentifier(), true);
    Assertions.assertEquals(8, countAllObjectRel(role1.id()));
    Assertions.assertEquals(0, countActiveObjectRel(role1.id()));

    roleMetaService.deleteRole(role1.nameIdentifier());

    // Test to delete schema with cascade mode
    catalog =
        createCatalog(
            RandomIdGenerator.INSTANCE.nextId(), Namespace.of("metalake"), "catalog", AUDIT_INFO);
    backend.insert(catalog, false);

    schema =
        createSchemaEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog"),
            "schema",
            AUDIT_INFO);
    backend.insert(schema, false);

    fileset =
        createFilesetEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog", "schema"),
            "fileset",
            AUDIT_INFO);
    backend.insert(fileset, false);
    table =
        createTableEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog", "schema"),
            "table",
            AUDIT_INFO);
    backend.insert(table, false);
    topic =
        createTopicEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog", "schema"),
            "topic",
            AUDIT_INFO);
    backend.insert(topic, false);
    model =
        createModelEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog", "schema"),
            "model",
            "comment",
            1,
            null,
            AUDIT_INFO);
    backend.insert(model, false);

    view =
        createViewEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog", "schema"),
            "view");
    backend.insert(view, false);
    FunctionEntity function3 =
        createFunctionEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            Namespace.of("metalake", "catalog", "schema"),
            "function",
            AUDIT_INFO);
    backend.insert(function3, false);

    role1 =
        createRoleEntity(
            RandomIdGenerator.INSTANCE.nextId(),
            AuthorizationUtils.ofRoleNamespace(metalakeName),
            "role1",
            AUDIT_INFO,
            Lists.newArrayList(
                catalogObject,
                schemaObject,
                tableObject,
                filesetObject,
                topicObject,
                modelObject,
                viewObject,
                functionObject),
            ImmutableMap.of("k1", "v1"));

    roleMetaService.insertRole(role1, false);

    SchemaMetaService.getInstance().deleteSchema(schema.nameIdentifier(), true);
    Assertions.assertEquals(8, countAllObjectRel(role1.id()));
    Assertions.assertEquals(1, countActiveObjectRel(role1.id()));
  }

  private Integer countAllObjectRel(Long roleId) {
    try (SqlSession sqlSession =
            SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
        Connection connection = sqlSession.getConnection();
        Statement statement1 = connection.createStatement();
        ResultSet rs1 =
            statement1.executeQuery(
                String.format(
                    "SELECT count(*) FROM role_meta_securable_object WHERE role_id = %d",
                    roleId))) {
      if (rs1.next()) {
        return rs1.getInt(1);
      } else {
        throw new RuntimeException("Doesn't contain data");
      }
    } catch (SQLException se) {
      throw new RuntimeException("SQL execution failed", se);
    }
  }

  private Integer countActiveObjectRel(Long roleId) {
    try (SqlSession sqlSession =
            SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
        Connection connection = sqlSession.getConnection();
        Statement statement1 = connection.createStatement();
        ResultSet rs1 =
            statement1.executeQuery(
                String.format(
                    "SELECT count(*) FROM role_meta_securable_object WHERE role_id = %d AND deleted_at = 0",
                    roleId))) {
      if (rs1.next()) {
        return rs1.getInt(1);
      } else {
        throw new RuntimeException("Doesn't contain data");
      }
    } catch (SQLException se) {
      throw new RuntimeException("SQL execution failed", se);
    }
  }
}
