/*
 * Copyright 2015-present Open Networking Foundation
 *
 * 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
 *
 *     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.opencord.aaa.impl;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.onlab.junit.TestUtils;
import org.onlab.packet.BasePacket;
import org.onlab.packet.DeserializationException;
import org.onlab.packet.EAP;
import org.onlab.packet.Ethernet;
import org.onlab.packet.IpAddress;
import org.onlab.packet.RADIUS;
import org.onlab.packet.RADIUSAttribute;
import org.onosproject.cluster.ClusterServiceAdapter;
import org.onosproject.cluster.LeadershipServiceAdapter;
import org.onosproject.cluster.NodeId;
import org.onosproject.core.CoreServiceAdapter;
import org.onosproject.event.DefaultEventSinkRegistry;
import org.onosproject.event.Event;
import org.onosproject.event.EventDeliveryService;
import org.onosproject.event.EventSink;
import org.onosproject.net.config.Config;
import org.onosproject.net.config.NetworkConfigRegistryAdapter;
import org.onosproject.net.packet.InboundPacket;
import org.onosproject.store.cluster.messaging.ClusterCommunicationServiceAdapter;
import org.onosproject.store.service.TestStorageService;
import org.opencord.aaa.AaaConfig;
import org.slf4j.Logger;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.atomic.AtomicReference;

import static com.google.common.base.Preconditions.checkState;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.onosproject.net.intent.TestTools.assertAfter;
import static org.slf4j.LoggerFactory.getLogger;

/**
 * Set of tests of the ONOS application component.
 */
@RunWith(Parameterized.class)
public class AaaManagerTest extends AaaTestBase {

    // Change this to have more run with mvn
    @Parameterized.Parameters
    public static Object[][] data() {
        return new Object[1][0];
    }

    static final String BAD_IP_ADDRESS = "198.51.100.0";

    private final Logger log = getLogger(getClass());

    private AaaManager aaaManager;
    private AaaStatisticsManager aaaStatisticsManager;

    class AaaManagerWithoutRadiusServer extends AaaManager {
        protected void sendRadiusPacket(RADIUS radiusPacket, InboundPacket inPkt) {
            savePacket(radiusPacket);
        }
    }
    /**
     * Mocks the AAAConfig class to force usage of an unroutable address for the
     * RADIUS server.
     */
    static class MockAaaConfig extends AaaConfig {
        @Override
        public InetAddress radiusIp() {
            try {
                return InetAddress.getByName(BAD_IP_ADDRESS);
            } catch (UnknownHostException ex) {
                // can't happen
                throw new IllegalStateException(ex);
            }
        }
    }

    static final class TestLeadershipService extends LeadershipServiceAdapter {
        @Override
        public NodeId getLeader(String path) {
            return new ClusterServiceAdapter().getLocalNode().id();
        }
    }

    /**
     * Mocks the network config registry.
     */
    @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
    private static final class TestNetworkConfigRegistry
            extends NetworkConfigRegistryAdapter {
        @Override
        public <S, C extends Config<S>> C getConfig(S subject, Class<C> configClass) {
            AaaConfig aaaConfig = new MockAaaConfig();
            return (C) aaaConfig;
        }
    }

    public static class TestEventDispatcher extends DefaultEventSinkRegistry
            implements EventDeliveryService {
        @Override
        @SuppressWarnings("unchecked")
        public synchronized void post(Event event) {
            EventSink sink = getSink(event.getClass());
            checkState(sink != null, "No sink for event %s", event);
            sink.process(event);
        }

        @Override
        public void setDispatchTimeLimit(long millis) {
        }

        @Override
        public long getDispatchTimeLimit() {
            return 0;
        }
    }

    /**
     * Sets up the services required by the AAA application.
     */
    @Before
    public void setUp() {
        aaaManager = new AaaManagerWithoutRadiusServer();
        aaaManager.netCfgService = new TestNetworkConfigRegistry();
        aaaManager.coreService = new CoreServiceAdapter();
        aaaManager.packetService = new MockPacketService();
        aaaManager.deviceService = new TestDeviceService();
        aaaManager.sadisService = new MockSadisService();
        aaaManager.cfgService = new MockCfgService();
        aaaManager.storageService = new TestStorageService();
        aaaStatisticsManager = new AaaStatisticsManager();
        aaaStatisticsManager.storageService = new TestStorageService();
        aaaStatisticsManager.clusterService = new ClusterServiceAdapter();
        aaaStatisticsManager.leadershipService = new TestLeadershipService();
        aaaStatisticsManager.clusterCommunicationService = new ClusterCommunicationServiceAdapter();
        aaaManager.radiusOperationalStatusService = new RadiusOperationalStatusManager();
        TestUtils.setField(aaaStatisticsManager, "eventDispatcher", new TestEventDispatcher());
        aaaStatisticsManager.activate(new MockComponentContext());
        aaaManager.aaaStatisticsManager = this.aaaStatisticsManager;
        TestUtils.setField(aaaManager, "eventDispatcher", new TestEventDispatcher());


        aaaManager.activate(new AaaTestBase.MockComponentContext());
    }

    /**
     * Tears down the AAA application.
     */
    @After
    public void tearDown() {
        aaaManager.deactivate(new AaaTestBase.MockComponentContext());
    }

    /**
     * Extracts the RADIUS packet from a packet sent by the supplicant.
     *
     * @param radius RADIUS packet sent by the supplicant
     * @throws DeserializationException if deserialization of the packet contents
     *         fails.
     */
    private void checkRadiusPacketFromSupplicant(RADIUS radius)
            throws DeserializationException {
        assertThat(radius, notNullValue());

        EAP eap = radius.decapsulateMessage();
        assertThat(eap, notNullValue());
    }

    /**
     * Fetches the sent packet at the given index. The requested packet
     * must be the last packet on the list.
     *
     * @param index index into sent packets array
     * @return packet
     */
    private BasePacket fetchPacket(int index) {
        BasePacket packet = savedPackets.get(index);
        assertThat(packet, notNullValue());
        return packet;
    }

    /**
     * Tests the authentication path through the AAA application.
     *
     * @throws DeserializationException if packed deserialization fails.
     */
    @Test
    public void testAuthentication() throws Exception {

        //  (1) Supplicant start up

        Ethernet startPacket = constructSupplicantStartPacket();
        sendPacket(startPacket);
        assertAfter(ASSERTION_DELAY, ASSERTION_LENGTH, () -> {
            Ethernet responsePacket = (Ethernet) fetchPacket(0);
            checkRadiusPacket(aaaManager, responsePacket, EAP.ATTR_IDENTITY);

            //  (2) Supplicant identify

            Ethernet identifyPacket = null;
            try {
                identifyPacket = constructSupplicantIdentifyPacket(null,
                                                                   EAP.ATTR_IDENTITY, (byte) 3, null);
            } catch (Exception e) {
                log.error(e.getMessage());
                fail();
            }
            sendPacket(identifyPacket);
        });
        assertAfter(ASSERTION_DELAY, ASSERTION_LENGTH, () -> {
            RADIUS radiusIdentifyPacket = (RADIUS) fetchPacket(1);
            AtomicReference<Byte> reqId = new AtomicReference<>(radiusIdentifyPacket.getIdentifier());

            try {
                checkRadiusPacketFromSupplicant(radiusIdentifyPacket);
            } catch (DeserializationException e) {
                log.error(e.getMessage());
                fail();
            }

            assertThat(radiusIdentifyPacket.getCode(), is(RADIUS.RADIUS_CODE_ACCESS_REQUEST));
            assertThat(new String(radiusIdentifyPacket.getAttribute(RADIUSAttribute.RADIUS_ATTR_USERNAME).getValue()),
                       is("testuser"));

            IpAddress nasIp =
                    IpAddress.valueOf(IpAddress.Version.INET,
                                      radiusIdentifyPacket.getAttribute(RADIUSAttribute.RADIUS_ATTR_NAS_IP)
                                              .getValue());
            assertThat(nasIp.toString(), is(aaaManager.nasIpAddress.getHostAddress()));

            // (3) RADIUS MD5 challenge

            RADIUS radiusCodeAccessChallengePacket =
                    constructRadiusCodeAccessChallengePacket(RADIUS.RADIUS_CODE_ACCESS_CHALLENGE, EAP.ATTR_MD5,
                                                             reqId.get(), aaaManager.radiusSecret.getBytes());
            aaaManager.handleRadiusPacket(radiusCodeAccessChallengePacket);
        });
        assertAfter(ASSERTION_DELAY, ASSERTION_LENGTH, () -> {

            //  State machine should have been created by now

            StateMachine stateMachine = aaaManager.getStateMachine(SESSION_ID);
            assertThat(stateMachine, notNullValue());
            assertThat(stateMachine.state(), is(StateMachine.STATE_PENDING));

            Ethernet radiusChallengeMD5Packet = (Ethernet) fetchPacket(2);
            checkRadiusPacket(aaaManager, radiusChallengeMD5Packet, EAP.ATTR_MD5);

            // (4) Supplicant MD5 response
            try {
                Ethernet md5RadiusPacket =
                        constructSupplicantIdentifyPacket(stateMachine,
                                                          EAP.ATTR_MD5,
                                                          stateMachine.challengeIdentifier(),
                                                          radiusChallengeMD5Packet);
                sendPacket(md5RadiusPacket);
            } catch (Exception e) {
                log.error(e.getMessage());
                fail();
            }
        });

        //  State machine should have been created by now

        StateMachine stateMachine = aaaManager.getStateMachine(SESSION_ID);

        assertAfter(ASSERTION_DELAY, ASSERTION_LENGTH, () -> {


            RADIUS responseMd5RadiusPacket = (RADIUS) fetchPacket(3);
            try {
                checkRadiusPacketFromSupplicant(responseMd5RadiusPacket);
            } catch (Exception e) {
                log.error(e.getMessage());
                fail();
            }
            //assertThat(responseMd5RadiusPacket.getIdentifier(), is((byte) 9));
            AtomicReference<Byte> reqId = new AtomicReference<>(responseMd5RadiusPacket.getIdentifier());
            assertThat(responseMd5RadiusPacket.getCode(), is(RADIUS.RADIUS_CODE_ACCESS_REQUEST));

            //  State machine should be in pending state

            assertThat(stateMachine, notNullValue());
            assertThat(stateMachine.state(), is(StateMachine.STATE_PENDING));

            // (5) RADIUS Success

            RADIUS successPacket =
                    constructRadiusCodeAccessChallengePacket(RADIUS.RADIUS_CODE_ACCESS_ACCEPT,
                                                             EAP.SUCCESS, reqId.get(),
                                                             aaaManager.radiusSecret.getBytes());
            aaaManager.handleRadiusPacket((successPacket));
        });
        assertAfter(ASSERTION_DELAY, ASSERTION_LENGTH, () -> {
            Ethernet supplicantSuccessPacket = (Ethernet) fetchPacket(4);

            checkRadiusPacket(aaaManager, supplicantSuccessPacket, EAP.SUCCESS);

            //  State machine should be in authorized state

            assertThat(stateMachine, notNullValue());
            assertThat(stateMachine.state(), is(StateMachine.STATE_AUTHORIZED));
        });
    }

    @Test
    public void testRemoveAuthentication() {
        Ethernet startPacket = constructSupplicantStartPacket();
        sendPacket(startPacket);
        assertAfter(ASSERTION_DELAY, ASSERTION_LENGTH, () -> {
            StateMachine stateMachine = aaaManager.getStateMachine(SESSION_ID);

            assertThat(stateMachine, notNullValue());
            assertThat(stateMachine.state(), is(StateMachine.STATE_STARTED));

            aaaManager.removeAuthenticationStateByMac(stateMachine.supplicantAddress());

            assertThat(aaaManager.getStateMachine(SESSION_ID), nullValue());
        });
    }

    /**
     * Tests the default configuration.
     */
    @Test
    public void testConfig() {
        assertThat(aaaManager.nasIpAddress.getHostAddress(), is(AaaConfig.DEFAULT_NAS_IP));
        assertThat(aaaManager.nasMacAddress, is(AaaConfig.DEFAULT_NAS_MAC));
        assertThat(aaaManager.radiusIpAddress.getHostAddress(), is(BAD_IP_ADDRESS));
        assertThat(aaaManager.radiusMacAddress, is(AaaConfig.DEFAULT_RADIUS_MAC));
    }
}
