/*
 * 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.server.authorization;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;

import java.lang.reflect.Field;
import java.util.concurrent.Executor;
import org.apache.gravitino.Config;
import org.apache.gravitino.Configs;
import org.apache.gravitino.Entity;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.UserPrincipal;
import org.apache.gravitino.dto.tag.MetadataObjectDTO;
import org.apache.gravitino.utils.NameIdentifierUtil;
import org.apache.gravitino.utils.PrincipalUtils;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;

/** Test of {@link MetadataAuthzHelper} */
public class TestMetadataAuthzHelper {

  private static MockedStatic<GravitinoEnv> mockedStaticGravitinoEnv;

  @BeforeAll
  public static void setup() {
    mockedStaticGravitinoEnv = mockStatic(GravitinoEnv.class);
    GravitinoEnv gravitinoEnv = mock(GravitinoEnv.class);
    mockedStaticGravitinoEnv.when(GravitinoEnv::getInstance).thenReturn(gravitinoEnv);
    Config configMock = mock(Config.class);
    when(gravitinoEnv.config()).thenReturn(configMock);
    when(configMock.get(eq(Configs.ENABLE_AUTHORIZATION))).thenReturn(true);
    when(configMock.get(eq(Configs.CACHE_ENABLED))).thenReturn(false);
  }

  @AfterAll
  public static void stop() {
    if (mockedStaticGravitinoEnv != null) {
      mockedStaticGravitinoEnv.close();
    }
  }

  @Test
  public void testFilterByExpression() {
    makeCompletableFutureUseCurrentThread();
    try (MockedStatic<PrincipalUtils> principalUtilsMocked = mockStatic(PrincipalUtils.class);
        MockedStatic<GravitinoAuthorizerProvider> mockStatic =
            mockStatic(GravitinoAuthorizerProvider.class)) {
      principalUtilsMocked
          .when(PrincipalUtils::getCurrentPrincipal)
          .thenReturn(new UserPrincipal("tester"));
      principalUtilsMocked.when(() -> PrincipalUtils.doAs(any(), any())).thenCallRealMethod();

      GravitinoAuthorizerProvider mockedProvider = mock(GravitinoAuthorizerProvider.class);
      mockStatic.when(GravitinoAuthorizerProvider::getInstance).thenReturn(mockedProvider);
      when(mockedProvider.getGravitinoAuthorizer()).thenReturn(new MockGravitinoAuthorizer());
      NameIdentifier[] nameIdentifiers = new NameIdentifier[3];
      nameIdentifiers[0] = NameIdentifierUtil.ofSchema("testMetalake", "testCatalog", "testSchema");
      nameIdentifiers[1] =
          NameIdentifierUtil.ofSchema("testMetalake", "testCatalog", "testSchema2");
      nameIdentifiers[2] =
          NameIdentifierUtil.ofSchema("testMetalake", "testCatalog2", "testSchema");
      NameIdentifier[] filtered =
          MetadataAuthzHelper.filterByExpression(
              "testMetalake",
              "CATALOG::USE_CATALOG && SCHEMA::USE_SCHEMA",
              Entity.EntityType.SCHEMA,
              nameIdentifiers);
      Assertions.assertEquals(1, filtered.length);
      Assertions.assertEquals("testMetalake.testCatalog.testSchema", filtered[0].toString());
      NameIdentifier[] filtered2 =
          MetadataAuthzHelper.filterByExpression(
              "testMetalake", "CATALOG::USE_CATALOG", Entity.EntityType.SCHEMA, nameIdentifiers);
      Assertions.assertEquals(2, filtered2.length);
      Assertions.assertEquals("testMetalake.testCatalog.testSchema", filtered2[0].toString());
      Assertions.assertEquals("testMetalake.testCatalog.testSchema2", filtered2[1].toString());
    }
  }

  @Test
  public void testFilterMetadataObject() {
    makeCompletableFutureUseCurrentThread();
    try (MockedStatic<PrincipalUtils> principalUtilsMocked = mockStatic(PrincipalUtils.class);
        MockedStatic<GravitinoAuthorizerProvider> mockStatic =
            mockStatic(GravitinoAuthorizerProvider.class)) {
      principalUtilsMocked
          .when(PrincipalUtils::getCurrentPrincipal)
          .thenReturn(new UserPrincipal("tester"));
      principalUtilsMocked.when(() -> PrincipalUtils.doAs(any(), any())).thenCallRealMethod();

      GravitinoAuthorizerProvider mockedProvider = mock(GravitinoAuthorizerProvider.class);
      mockStatic.when(GravitinoAuthorizerProvider::getInstance).thenReturn(mockedProvider);
      when(mockedProvider.getGravitinoAuthorizer()).thenReturn(new MockGravitinoAuthorizer());

      // Create test MetadataObjectDTO instances
      MetadataObjectDTO[] metadataObjects = new MetadataObjectDTO[3];
      metadataObjects[0] =
          MetadataObjectDTO.builder()
              .withName("testSchema")
              .withParent("testCatalog")
              .withType(MetadataObject.Type.SCHEMA)
              .build();
      metadataObjects[1] =
          MetadataObjectDTO.builder()
              .withName("testSchema2")
              .withParent("testCatalog")
              .withType(MetadataObject.Type.SCHEMA)
              .build();
      metadataObjects[2] =
          MetadataObjectDTO.builder()
              .withName("testSchema3")
              .withParent("testCatalog2")
              .withType(MetadataObject.Type.SCHEMA)
              .build();

      MetadataObjectDTO[] filtered =
          MetadataAuthzHelper.filterMetadataObject("testMetalake", metadataObjects);

      // Based on the MockGravitinoAuthorizer, 2 of the 3 schemas should be accessible
      Assertions.assertEquals(1, filtered.length);
      Assertions.assertEquals("testSchema", filtered[0].name());
    }
  }

  @Test
  public void testFilterMetadataObjectDTO() {
    makeCompletableFutureUseCurrentThread();
    try (MockedStatic<PrincipalUtils> principalUtilsMocked = mockStatic(PrincipalUtils.class);
        MockedStatic<GravitinoAuthorizerProvider> mockStatic =
            mockStatic(GravitinoAuthorizerProvider.class)) {
      principalUtilsMocked
          .when(PrincipalUtils::getCurrentPrincipal)
          .thenReturn(new UserPrincipal("tester"));
      principalUtilsMocked.when(() -> PrincipalUtils.doAs(any(), any())).thenCallRealMethod();

      GravitinoAuthorizerProvider mockedProvider = mock(GravitinoAuthorizerProvider.class);
      mockStatic.when(GravitinoAuthorizerProvider::getInstance).thenReturn(mockedProvider);
      when(mockedProvider.getGravitinoAuthorizer()).thenReturn(new MockGravitinoAuthorizer());

      // Create test MetadataObjectDTO instances
      MetadataObjectDTO[] metadataObjects = new MetadataObjectDTO[3];
      metadataObjects[0] =
          MetadataObjectDTO.builder()
              .withName("testSchema")
              .withParent("testCatalog")
              .withType(MetadataObject.Type.SCHEMA)
              .build();
      metadataObjects[1] =
          MetadataObjectDTO.builder()
              .withName("testSchema2")
              .withParent("testCatalog")
              .withType(MetadataObject.Type.SCHEMA)
              .build();
      metadataObjects[2] =
          MetadataObjectDTO.builder()
              .withName("testSchema3")
              .withParent("testCatalog")
              .withType(MetadataObject.Type.SCHEMA)
              .build();

      MetadataObjectDTO[] filtered =
          MetadataAuthzHelper.filterMetadataObject("testMetalake", metadataObjects);

      // Based on the MockGravitinoAuthorizer, 2 of the 3 tables should be accessible
      Assertions.assertEquals(1, filtered.length);
      Assertions.assertEquals("testSchema", filtered[0].name());
    }
  }

  private static void makeCompletableFutureUseCurrentThread() {
    try {
      Executor currentThread = Runnable::run;
      Class<MetadataAuthzHelper> jcasbinAuthorizerClass = MetadataAuthzHelper.class;
      Field field = jcasbinAuthorizerClass.getDeclaredField("executor");
      field.setAccessible(true);
      field.set(null, currentThread);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
}
