package com.cloudhandson.vpdbackoffice.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.groups.Tuple.tuple;

import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent;
import com.cloudhandson.vpdbackoffice.domain.permission.AppRole;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionRule;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSet;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSetCommand;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionView;
import com.cloudhandson.vpdbackoffice.domain.permission.RuleCommand;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
import com.cloudhandson.vpdbackoffice.mapper.AuditMapper;
import com.cloudhandson.vpdbackoffice.mapper.PermissionMapper;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class PermissionServiceTest {

  private PermissionMapper permissionMapper;
  private ProtectedObjectService protectedObjectService;
  private PermissionService permissionService;

  @BeforeEach
  void setUp() {
    permissionMapper = new FakePermissionMapper();
    AuditService auditService = new AuditService(new NoopAuditMapper());
    protectedObjectService = new ProtectedObjectService(null, auditService) {
      @Override
      public ProtectedObject assertEnabled(long objectId) {
        return new ProtectedObject(1L, "ADMIN", "CB_V_SEARCH_DOCUMENTS", "cb-agent-security/vpd/documents", "Y");
      }

      @Override
      public List<ProtectedColumn> findColumns(long objectId) {
        return List.of(
            new ProtectedColumn(1L, 1L, "DEPT_CODE", "N", null, "INTERNAL", "NONE"),
            new ProtectedColumn(2L, 1L, "OWNER_EMP_NO", "N", null, "INTERNAL", "NONE"),
            new ProtectedColumn(3L, 1L, "TECH_TAG", "N", null, "PUBLIC", "NONE")
        );
      }
    };
    permissionService = new PermissionService(permissionMapper, protectedObjectService, auditService);
  }

  @Test
  void rejectsAllRuleMixedWithSpecificRule() {
    var command = new PermissionSetCommand(
        10L,
        1L,
        "SELECT",
        "ALLOW",
        List.of(new RuleCommand(null, "ALL", null), new RuleCommand("DEPT_CODE", "=", "APAC")),
        List.of()
    );

    assertThatThrownBy(() -> permissionService.savePermissionSet(command))
        .isInstanceOf(AppException.class)
        .hasMessageContaining("ALL");
  }

  @Test
  void rejectsCustomPredicateRule() {
    var command = new PermissionSetCommand(
        10L,
        1L,
        "SELECT",
        "ALLOW",
        List.of(new RuleCommand("DEPT_CODE", "CUSTOM_PREDICATE", "1=1")),
        List.of()
    );

    assertThatThrownBy(() -> permissionService.savePermissionSet(command))
        .isInstanceOf(AppException.class)
        .hasMessageContaining("허용되지 않은");
  }

  @Test
  void allowsDefaultColumnsForContextRules() {
    var command = new PermissionSetCommand(
        10L,
        1L,
        "SELECT",
        "ALLOW",
        List.of(new RuleCommand(null, "MY_DEPT", null), new RuleCommand(null, "SELF", null)),
        List.of()
    );

    permissionService.savePermissionSet(command);

    FakePermissionMapper mapper = (FakePermissionMapper) permissionMapper;
    assertThat(mapper.insertedRules)
        .extracting(PermissionRule::ruleType)
        .containsExactly("MY_DEPT", "SELF");
    assertThat(mapper.insertedRules)
        .extracting(PermissionRule::ruleColumn)
        .containsExactly(null, null);
  }

  @Test
  void acceptsDeptAndEmpNoRulesWithDefaultColumnsAndValues() {
    var command = new PermissionSetCommand(
        10L,
        1L,
        "SELECT",
        "ALLOW",
        List.of(new RuleCommand(null, "DEPT", "HR"), new RuleCommand(null, "EMP_NO", "E2001")),
        List.of()
    );

    permissionService.savePermissionSet(command);

    FakePermissionMapper mapper = (FakePermissionMapper) permissionMapper;
    assertThat(mapper.insertedRules)
        .extracting(PermissionRule::ruleType)
        .containsExactly("DEPT", "EMP_NO");
    assertThat(mapper.insertedRules)
        .extracting(PermissionRule::ruleValue)
        .containsExactly("HR", "E2001");
  }

  @Test
  void rejectsDeptRuleWithoutValue() {
    var command = new PermissionSetCommand(
        10L,
        1L,
        "SELECT",
        "ALLOW",
        List.of(new RuleCommand(null, "DEPT", "")),
        List.of()
    );

    assertThatThrownBy(() -> permissionService.savePermissionSet(command))
        .isInstanceOf(AppException.class)
        .hasMessageContaining("값이 필요");
  }

  @Test
  void acceptsMultipleTagRulesAsOrCandidates() {
    var command = new PermissionSetCommand(
        10L,
        1L,
        "SELECT",
        "ALLOW",
        List.of(
            new RuleCommand(null, "TAG", "spring_boot"),
            new RuleCommand(null, "TAG", "oracle_vpd")
        ),
        List.of()
    );

    permissionService.savePermissionSet(command);

    FakePermissionMapper mapper = (FakePermissionMapper) permissionMapper;
    assertThat(mapper.insertedRules)
        .extracting(PermissionRule::ruleType)
        .containsExactly("TAG", "TAG");
    assertThat(mapper.insertedRules)
        .extracting(PermissionRule::ruleColumn)
        .containsExactly(null, null);
    assertThat(mapper.insertedRules)
        .extracting(PermissionRule::ruleValue)
        .containsExactly("SPRING_BOOT", "ORACLE_VPD");
  }

  @Test
  void acceptsStakeholderContextRulesWithAnExplicitObjectColumn() {
    var command = new PermissionSetCommand(
        10L,
        1L,
        "SELECT",
        "ALLOW",
        List.of(
            new RuleCommand("DEPT_CODE", "STAKEHOLDER_SELF", "설계사"),
            new RuleCommand("OWNER_EMP_NO", "STAKEHOLDER_CHANNEL", "지점장")
        ),
        List.of()
    );

    permissionService.savePermissionSet(command);

    FakePermissionMapper mapper = (FakePermissionMapper) permissionMapper;
    assertThat(mapper.insertedRules)
        .extracting(PermissionRule::ruleType)
        .containsExactly("STAKEHOLDER_SELF", "STAKEHOLDER_CHANNEL");
  }

  @Test
  void rejectsStakeholderContextRuleWithoutAnObjectColumn() {
    var command = new PermissionSetCommand(
        10L,
        1L,
        "SELECT",
        "ALLOW",
        List.of(new RuleCommand(null, "STAKEHOLDER_SELF", null)),
        List.of()
    );

    assertThatThrownBy(() -> permissionService.savePermissionSet(command))
        .isInstanceOf(AppException.class)
        .hasMessageContaining("컬럼이 필요");
  }

  @Test
  void rejectsStakeholderContextRuleWithoutExpectedRole() {
    var command = new PermissionSetCommand(
        10L,
        1L,
        "SELECT",
        "ALLOW",
        List.of(new RuleCommand("DEPT_CODE", "STAKEHOLDER_SELF", null)),
        List.of()
    );

    assertThatThrownBy(() -> permissionService.savePermissionSet(command))
        .isInstanceOf(AppException.class)
        .hasMessageContaining("값이 필요");
  }

  @Test
  void acceptsKbFilterConditionCodesWithoutAStaticRuleValue() {
    var command = new PermissionSetCommand(
        10L,
        1L,
        "SELECT",
        "ALLOW",
        List.of(
            new RuleCommand("DEPT_CODE", "OWN_CONTRACT", null),
            new RuleCommand("OWNER_EMP_NO", "CHANNEL_CUSTOMER", null)
        ),
        List.of()
    );

    permissionService.savePermissionSet(command);

    FakePermissionMapper mapper = (FakePermissionMapper) permissionMapper;
    assertThat(mapper.insertedRules)
        .extracting(PermissionRule::ruleType)
        .containsExactly("OWN_CONTRACT", "CHANNEL_CUSTOMER");
    assertThat(mapper.insertedRules)
        .extracting(PermissionRule::ruleValue)
        .containsExactly("", "");
  }

  @Test
  void acceptsStaticSqlPredicateAlongsideAContextCondition() {
    var command = new PermissionSetCommand(
        10L,
        1L,
        "SELECT",
        "ALLOW",
        List.of(
            new RuleCommand("DEPT_CODE", "OWN_CONTRACT", null),
            new RuleCommand(null, "STATIC_SQL", "DEPT_CODE IN ('HR', 'FIN')")
        ),
        List.of()
    );

    permissionService.savePermissionSet(command);

    FakePermissionMapper mapper = (FakePermissionMapper) permissionMapper;
    assertThat(mapper.insertedRules)
        .extracting(PermissionRule::ruleType)
        .containsExactly("OWN_CONTRACT", "STATIC_SQL");
    assertThat(mapper.insertedRules)
        .extracting(PermissionRule::ruleValue)
        .containsExactly("", "DEPT_CODE IN ('HR', 'FIN')");
  }

  @Test
  void acceptsExplicitUnconditionalStaticSqlPredicateForAdminRole() {
    var command = new PermissionSetCommand(
        10L,
        1L,
        "SELECT",
        "ALLOW",
        List.of(new RuleCommand(null, "STATIC_SQL", "1 = 1")),
        List.of()
    );

    permissionService.savePermissionSet(command);

    FakePermissionMapper mapper = (FakePermissionMapper) permissionMapper;
    assertThat(mapper.insertedRules)
        .extracting(PermissionRule::ruleType, PermissionRule::ruleValue)
        .containsExactly(tuple("STATIC_SQL", "1 = 1"));
  }

  @Test
  void rejectsStaticSqlStatementOrUnknownColumn() {
    var statementCommand = new PermissionSetCommand(
        10L,
        1L,
        "SELECT",
        "ALLOW",
        List.of(new RuleCommand(null, "STATIC_SQL", "DEPT_CODE = 'HR'; DELETE FROM CB_APP_USER")),
        List.of()
    );
    var unknownColumnCommand = new PermissionSetCommand(
        10L,
        1L,
        "SELECT",
        "ALLOW",
        List.of(new RuleCommand(null, "STATIC_SQL", "UNSUPPORTED_COLUMN = 'HR'")),
        List.of()
    );

    assertThatThrownBy(() -> permissionService.savePermissionSet(statementCommand))
        .isInstanceOf(AppException.class)
        .hasMessageContaining("단일 WHERE");
    assertThatThrownBy(() -> permissionService.savePermissionSet(unknownColumnCommand))
        .isInstanceOf(AppException.class)
        .hasMessageContaining("보호 객체 컬럼");
  }

  @Test
  void ignoresVisibleColumnsBecauseAsoHandlesColumnMasking() {
    var command = new PermissionSetCommand(
        10L,
        1L,
        "SELECT",
        "ALLOW",
        List.of(new RuleCommand("DEPT_CODE", "=", "HR")),
        List.of("DEPT_CODE", "OWNER_EMP_NO")
    );

    permissionService.savePermissionSet(command);

    FakePermissionMapper mapper = (FakePermissionMapper) permissionMapper;
    assertThat(mapper.insertedVisibleColumns).isEmpty();
  }

  @Test
  void rejectsTagRuleWithoutValue() {
    var command = new PermissionSetCommand(
        10L,
        1L,
        "SELECT",
        "ALLOW",
        List.of(new RuleCommand(null, "TAG", "")),
        List.of()
    );

    assertThatThrownBy(() -> permissionService.savePermissionSet(command))
        .isInstanceOf(AppException.class)
        .hasMessageContaining("값이 필요");
  }

  @Test
  void rejectsUnsafeTagValue() {
    var command = new PermissionSetCommand(
        10L,
        1L,
        "SELECT",
        "ALLOW",
        List.of(new RuleCommand(null, "TAG", "SPRING' OR '1'='1")),
        List.of()
    );

    assertThatThrownBy(() -> permissionService.savePermissionSet(command))
        .isInstanceOf(AppException.class)
        .hasMessageContaining("영문 대문자");
  }

  @Test
  void acceptsDenyPermissionEffect() {
    var command = new PermissionSetCommand(
        10L,
        1L,
        "SELECT",
        "DENY",
        List.of(new RuleCommand("DEPT_CODE", "=", "HR")),
        List.of()
    );

    permissionService.savePermissionSet(command);

    FakePermissionMapper mapper = (FakePermissionMapper) permissionMapper;
    assertThat(mapper.insertedEffect).isEqualTo("DENY");
  }

  @Test
  void disablesProtectedObjectWhenLastPermissionIsDeleted() {
    var mapper = new FakePermissionMapper();
    boolean[] disabled = {false};
    AuditService auditService = new AuditService(new NoopAuditMapper());
    ProtectedObjectService objectService = new ProtectedObjectService(null, auditService) {
      @Override
      public void disableObject(long objectId) {
        disabled[0] = objectId == 1L;
      }
    };
    PermissionService service = new PermissionService(mapper, objectService, auditService);

    service.deletePermission(1000L);

    assertThat(disabled[0]).isTrue();
  }

  @Test
  void rejectsPermissionDeleteWithoutImpactConfirmation() {
    assertThatThrownBy(() -> permissionService.deletePermission(1000L, false))
        .isInstanceOf(AppException.class)
        .hasMessageContaining("영향 확인");
  }

  @Test
  void blocksRoleDeleteWhenDependenciesRemain() {
    var mapper = new FakePermissionMapper();
    mapper.userRoleCount = 1;
    PermissionService service = new PermissionService(mapper, protectedObjectService, new AuditService(new NoopAuditMapper()));

    assertThatThrownBy(() -> service.deleteRole(10L, true))
        .isInstanceOf(AppException.class)
        .hasMessageContaining("먼저 해제");
  }

  private static class NoopAuditMapper implements AuditMapper {
    @Override
    public void insert(AuditEvent event) {
    }
  }

  private static class FakePermissionMapper implements PermissionMapper {
    private final List<PermissionRule> insertedRules = new ArrayList<>();
    private final List<String> insertedVisibleColumns = new ArrayList<>();

    @Override
    public List<AppRole> findRoles() {
      return List.of(new AppRole(10L, "HR_DEPT_ROLE", null, "INTERNAL"));
    }

    @Override
    public AppRole findRole(long roleId) {
      return roleId == 10L ? new AppRole(10L, "HR_DEPT_ROLE", null, "INTERNAL") : null;
    }

    @Override
    public long nextRoleId() {
      return 40L;
    }

    @Override
    public void insertRole(long roleId, String roleName, String description, String maxSensitivityLevel) {
    }

    @Override
    public int updateRoleMaxSensitivity(long roleId, String maxSensitivityLevel) {
      return 1;
    }

    @Override
    public int deleteRole(long roleId) {
      return 1;
    }

    private int userRoleCount;
    private int groupRoleCount;
    private int permissionCountByRole;

    @Override
    public int countUserRolesByRoleId(long roleId) {
      return userRoleCount;
    }

    @Override
    public int countGroupRolesByRoleId(long roleId) {
      return groupRoleCount;
    }

    @Override
    public int countPermissionsByRoleId(long roleId) {
      return permissionCountByRole;
    }

    @Override
    public List<PermissionView> findPermissionViews() {
      return List.of();
    }

    @Override
    public PermissionSet findPermissionSet(long roleId, long objectId) {
      return null;
    }

    @Override
    public Long findPermissionId(long roleId, long objectId) {
      return null;
    }

    @Override
    public Long findObjectIdByPermissionId(long permissionId) {
      return permissionId == 1000L ? 1L : null;
    }

    @Override
    public int countPermissionsByObjectId(long objectId) {
      return 0;
    }

    private String insertedEffect;

    @Override
    public void insertPermission(long permissionId, long roleId, long objectId, String action, String permissionEffect) {
      insertedEffect = permissionEffect;
    }

    @Override
    public void updatePermissionAction(long permissionId, String action) {
    }

    @Override
    public void updatePermissionEffect(long permissionId, String permissionEffect) {
      insertedEffect = permissionEffect;
    }

    @Override
    public void deleteRules(long permissionId) {
    }

    @Override
    public void insertRule(PermissionRule rule) {
      insertedRules.add(rule);
    }

    @Override
    public void deleteVisibleColumns(long permissionId) {
    }

    @Override
    public void insertVisibleColumn(long permissionId, String columnName) {
      insertedVisibleColumns.add(columnName);
    }

    @Override
    public int deletePermission(long permissionId) {
      return 1;
    }

    @Override
    public long nextPermissionId() {
      return 1000L;
    }

    @Override
    public long nextRuleId() {
      return 10000L;
    }
  }
}
