/*
 * Copyright 2004-present the original author or 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
 *
 *      https://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.springframework.security.acls.jdbc;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.sql.DataSource;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.domain.PrincipalSid;
import org.springframework.security.acls.model.Acl;
import org.springframework.security.acls.model.NotFoundException;
import org.springframework.security.acls.model.ObjectIdentity;
import org.springframework.security.acls.model.Sid;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;

/**
 * Unit and Integration tests the ACL JdbcAclService using an in-memory database.
 *
 * @author Nena Raab
 */
@ExtendWith(MockitoExtension.class)
public class JdbcAclServiceTests {

	private EmbeddedDatabase embeddedDatabase;

	@Mock
	private DataSource dataSource;

	@Mock
	private LookupStrategy lookupStrategy;

	@Mock
	JdbcOperations jdbcOperations;

	private JdbcAclService aclServiceIntegration;

	private JdbcAclService aclService;

	@BeforeEach
	public void setUp() {
		// @formatter:off
		this.embeddedDatabase = new EmbeddedDatabaseBuilder()
			.addScript("createAclSchemaWithAclClassIdType.sql")
			.addScript("db/sql/test_data_hierarchy.sql")
			.build();
		// @formatter:on

		this.aclService = new JdbcAclService(this.jdbcOperations, this.lookupStrategy);
		this.aclServiceIntegration = new JdbcAclService(this.embeddedDatabase, this.lookupStrategy);
	}

	@AfterEach
	public void tearDownEmbeddedDatabase() {
		this.embeddedDatabase.shutdown();
	}

	// SEC-1898
	@Test
	public void readAclByIdMissingAcl() {
		Map<ObjectIdentity, Acl> result = new HashMap<>();
		given(this.lookupStrategy.readAclsById(anyList(), anyList())).willReturn(result);
		ObjectIdentity objectIdentity = new ObjectIdentityImpl(Object.class, 1);
		List<Sid> sids = Arrays.<Sid>asList(new PrincipalSid("user"));
		assertThatExceptionOfType(NotFoundException.class)
			.isThrownBy(() -> this.aclService.readAclById(objectIdentity, sids));
	}

	@Test
	public void findOneChildren() {
		List<ObjectIdentity> result = new ArrayList<>();
		result.add(new ObjectIdentityImpl(Object.class, "5577"));
		Object[] args = { "1", "org.springframework.security.acls.jdbc.JdbcAclServiceTests$MockLongIdDomainObject" };
		given(this.jdbcOperations.query(anyString(), ArgumentMatchers.<RowMapper<ObjectIdentity>>any(), eq(args)))
			.willReturn(result);
		ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockLongIdDomainObject.class, 1L);
		List<ObjectIdentity> objectIdentities = this.aclService.findChildren(objectIdentity);
		assertThat(objectIdentities).hasSize(1);
		assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("5577");
	}

	@Test
	public void findNoChildren() {
		ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockLongIdDomainObject.class, 1L);
		List<ObjectIdentity> objectIdentities = this.aclService.findChildren(objectIdentity);
		assertThat(objectIdentities).isNull();
	}

	@Test
	public void findChildrenWithoutIdType() {
		ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockLongIdDomainObject.class, 4711L);
		List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
		assertThat(objectIdentities).hasSize(1);
		assertThat(objectIdentities.get(0).getType()).isEqualTo(MockUntypedIdDomainObject.class.getName());
		assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo(5000L);
	}

	@Test
	public void findChildrenForUnknownObject() {
		ObjectIdentity objectIdentity = new ObjectIdentityImpl(Object.class, 33);
		List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
		assertThat(objectIdentities).isNull();
	}

	@Test
	public void findChildrenOfIdTypeLong() {
		ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US-PAL");
		List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
		assertThat(objectIdentities).hasSize(2);
		assertThat(objectIdentities.get(0).getType()).isEqualTo(MockLongIdDomainObject.class.getName());
		assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo(4711L);
		assertThat(objectIdentities.get(1).getType()).isEqualTo(MockLongIdDomainObject.class.getName());
		assertThat(objectIdentities.get(1).getIdentifier()).isEqualTo(4712L);
	}

	@Test
	public void findChildrenOfIdTypeString() {
		ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US");
		this.aclServiceIntegration.setAclClassIdSupported(true);
		List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
		assertThat(objectIdentities).hasSize(1);
		assertThat(objectIdentities.get(0).getType()).isEqualTo("location");
		assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("US-PAL");
	}

	@Test
	public void findChildrenOfIdTypeUUID() {
		ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockUntypedIdDomainObject.class, 5000L);
		this.aclServiceIntegration.setAclClassIdSupported(true);
		List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
		assertThat(objectIdentities).hasSize(1);
		assertThat(objectIdentities.get(0).getType()).isEqualTo("costcenter");
		assertThat(objectIdentities.get(0).getIdentifier())
			.isEqualTo(UUID.fromString("25d93b3f-c3aa-4814-9d5e-c7c96ced7762"));
	}

	@Test
	public void setObjectIdentityGeneratorWhenNullThenThrowsIllegalArgumentException() {
		assertThatIllegalArgumentException()
			.isThrownBy(() -> this.aclServiceIntegration.setObjectIdentityGenerator(null))
			.withMessage("objectIdentityGenerator cannot be null");
	}

	@Test
	public void findChildrenWhenObjectIdentityGeneratorSetThenUsed() {
		this.aclServiceIntegration
			.setObjectIdentityGenerator((id, type) -> new ObjectIdentityImpl(type, "prefix:" + id));

		ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US");
		this.aclServiceIntegration.setAclClassIdSupported(true);
		List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
		assertThat(objectIdentities).hasSize(1);
		assertThat(objectIdentities.get(0).getType()).isEqualTo("location");
		assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("prefix:US-PAL");
	}

	class MockLongIdDomainObject {

		private Object id;

		Object getId() {
			return this.id;
		}

		void setId(Object id) {
			this.id = id;
		}

	}

	class MockUntypedIdDomainObject {

		private Object id;

		Object getId() {
			return this.id;
		}

		void setId(Object id) {
			this.id = id;
		}

	}

}
