/*
 *  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.causeway.security.simple;

import java.util.List;
import java.util.function.Consumer;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import org.apache.causeway.applib.Identifier;
import org.apache.causeway.applib.id.LogicalType;
import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.core.security.authentication.AuthenticationRequest;
import org.apache.causeway.core.security.authentication.AuthenticationRequestPassword;
import org.apache.causeway.security.simple.authentication.SimpleAuthenticator;
import org.apache.causeway.security.simple.authorization.SimpleAuthorizor;
import org.apache.causeway.security.simple.realm.SimpleRealm;
import org.apache.causeway.security.simple.realm.SimpleRealm.Grant;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;

class SecuritySimpleAuthTest {

    private PasswordEncoder passEncoder = new BCryptPasswordEncoder();

    private SimpleRealm realm = new SimpleRealm()
                //roles
                .addRole("admin_role", id->Grant.CHANGE)
                .addRole("order_role", id->
                    id.getFullIdentityString().contains("Order")
                        ? Grant.CHANGE
                        : Grant.NONE)
                .addRole("customer_role", id->
                    id.getFullIdentityString().contains("Customer")
                        ? Grant.CHANGE
                        : Grant.NONE)
                .addRole("reader_role", id->
                    id.getFullIdentityString().contains("TopSecret")
                        ? Grant.NONE
                        : Grant.READ)
                //users
                .addUser("sven", passEncoder.encode("pass0"), List.of("admin_role"))
                .addUser("dick", passEncoder.encode("pass1"), List.of("reader_role", "order_role"))
                .addUser("bob", passEncoder.encode("pass2"), List.of("reader_role", "customer_role"))
                .addUser("joe", passEncoder.encode("pass3"), List.of("reader_role"));

    private SimpleAuthenticator simpleAuthenticator = new SimpleAuthenticator(realm, passEncoder);
    private SimpleAuthorizor simpleAuthorizor = new SimpleAuthorizor(realm);

    // -- SCENARIOS

    static class Order {
        String name;
    }
    static class Customer {
        String name;
    }
    static class TopSecret {
        String name;
    }

    @RequiredArgsConstructor @Getter @Accessors(fluent=true)
    enum Scenario {
        SVEN(
                Can.of(ord(), cus(), top()), // read grant
                Can.of(ord(), cus(), top()), // change grant
                Can.empty(), // read deny
                Can.empty()), // change deny
        DICK(
                Can.of(ord(), cus()), // read grant
                Can.of(ord()), // change grant
                Can.of(top()), // read deny
                Can.of(cus(), top())), // change deny
        BOB(
                Can.of(ord(), cus()), // read grant
                Can.of(cus()), // change grant
                Can.of(top()), // read deny
                Can.of(ord(), top())), // change deny
        JOE(
                Can.of(ord(), cus()), // read grant
                Can.empty(), // change grant
                Can.of(top()), // read deny
                Can.of(ord(), cus(), top())); // change deny
        String userName() { return name().toLowerCase(); }
        String plainPass() { return "pass"+ordinal(); }
        final Can<Identifier> expectedReadGranted;
        final Can<Identifier> expectedChangeGranted;
        final Can<Identifier> expectedReadDenied;
        final Can<Identifier> expectedChangeDenied;
        static Identifier ord() {
            return Identifier.propertyIdentifier(LogicalType.fqcn(Order.class), "name");
        }
        static Identifier cus() {
            return Identifier.propertyIdentifier(LogicalType.fqcn(Customer.class), "name");
        }
        static Identifier top() {
            return Identifier.propertyIdentifier(LogicalType.fqcn(TopSecret.class), "name");
        }
    }

    // -- TESTS

    @ParameterizedTest
    @EnumSource(Scenario.class)
    void authentication(final Scenario scenario) {
        // when user is null, then invalid
        assertInvalid(new AuthenticationRequestPassword(null, "123xyz"));
        // when plain pass is null, then invalid
        assertInvalid(new AuthenticationRequestPassword(scenario.userName(), null));
        // when plain pass is empty, then invalid
        assertInvalid(new AuthenticationRequestPassword(scenario.userName(), ""));
        // when plain pass is random string, then invalid
        assertInvalid(new AuthenticationRequestPassword(scenario.userName(), "123xyz"));

        // happy case
        assertValid(new AuthenticationRequestPassword(scenario.userName(), scenario.plainPass()), interactionContext->{
            assertEquals(scenario.userName(), interactionContext.getUser().name());
        });
    }

    @ParameterizedTest
    @EnumSource(Scenario.class)
    void authorization(final Scenario scenario) {
        assertValid(new AuthenticationRequestPassword(scenario.userName(), scenario.plainPass()), interactionContext->{
            scenario.expectedReadGranted().forEach(grant->
                assertTrue(simpleAuthorizor.isVisible(interactionContext, grant)));
            scenario.expectedChangeGranted().forEach(grant->
                assertTrue(simpleAuthorizor.isUsable(interactionContext, grant)));
            scenario.expectedReadDenied().forEach(veto->
                assertFalse(simpleAuthorizor.isVisible(interactionContext, veto)));
            scenario.expectedChangeDenied().forEach(veto->
                assertFalse(simpleAuthorizor.isUsable(interactionContext, veto)));
        });
    }

    // -- HELPER

    void assertValid(final AuthenticationRequest request, final Consumer<InteractionContext> validator) {
        var interactionContext = simpleAuthenticator.authenticate(request , "test");
        assertNotNull(interactionContext);
        validator.accept(interactionContext);
    }

    void assertInvalid(final AuthenticationRequest request) {
        var interaction = simpleAuthenticator.authenticate(request , "test");
        assertNull(interaction);
    }

}
