/*_############################################################################
  _##
  _##  SNMP4J - DefaultUdpTransportMappingAuditTest.java
  _##
  _##  Copyright (C) 2003-2026  Frank Fock (SNMP4J.org)
  _##
  _##  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.snmp4j.transport;

import junit.framework.TestCase;
import org.snmp4j.TransportMapping;
import org.snmp4j.TransportStateReference;
import org.snmp4j.log.ConsoleLogAdapter;
import org.snmp4j.log.ConsoleLogFactory;
import org.snmp4j.log.LogAdapter;
import org.snmp4j.log.LogFactory;
import org.snmp4j.smi.Address;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.UdpAddress;
import org.snmp4j.transport.TransportAuditEvent.TransportOperation;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;

/**
 * Tests the {@link TransportAuditListener} support of {@link AbstractTransportMapping} using the
 * {@link DefaultUdpTransportMapping}: registration/{@link AbstractTransportMapping#isTransportAudit()},
 * auditing of {@link TransportOperation#LISTEN}, {@link TransportOperation#SEND},
 * {@link TransportOperation#RECEIVE} and {@link TransportOperation#CLOSE} events, and the ability of a
 * listener to deny (cancel) {@code LISTEN}, {@code SEND} and {@code RECEIVE} operations.
 *
 * @author Frank Fock
 * @since 3.12.0
 */
public class DefaultUdpTransportMappingAuditTest extends TestCase {

    static {
        LogFactory.setLogFactory(new ConsoleLogFactory());
        ConsoleLogAdapter.setWarnEnabled(true);
    }

    private static final LogAdapter LOGGER = LogFactory.getLogger(DefaultUdpTransportMappingAuditTest.class);

    private static final byte[] PAYLOAD = new OctetString("audit-test-payload").getValue();

    /**
     * A {@link TransportAuditListener} that records all received {@link TransportAuditEvent}s and can be
     * configured to deny a configurable set of {@link TransportOperation}s.
     */
    private static class RecordingAuditListener implements TransportAuditListener {

        private final List<TransportAuditEvent> events = Collections.synchronizedList(new ArrayList<>());
        private final EnumSet<TransportOperation> denied;
        private final Object sync;

        RecordingAuditListener(Object sync, TransportOperation... deniedOperations) {
            this.sync = sync;
            this.denied = EnumSet.noneOf(TransportOperation.class);
            Collections.addAll(this.denied, deniedOperations);
        }

        @Override
        public boolean transportAudit(TransportAuditEvent event) {
            LOGGER.info("Audit event: " + event);
            events.add(event);
            if (sync != null) {
                synchronized (sync) {
                    sync.notifyAll();
                }
            }
            return !denied.contains(event.getOperation());
        }

        List<TransportAuditEvent> eventsOf(TransportOperation operation) {
            List<TransportAuditEvent> result = new ArrayList<>();
            synchronized (events) {
                for (TransportAuditEvent event : events) {
                    if (event.getOperation() == operation) {
                        result.add(event);
                    }
                }
            }
            return result;
        }
    }

    /**
     * A {@link TransportListener} that records the received message bytes.
     */
    private static class RecordingTransportListener implements TransportListener {
        private final List<OctetString> received = Collections.synchronizedList(new ArrayList<>());
        private final Object sync;

        RecordingTransportListener(Object sync) {
            this.sync = sync;
        }

        @Override
        public <A extends Address> void processMessage(TransportMapping<? super A> sourceTransport,
                                                       A incomingAddress, ByteBuffer wholeMessage,
                                                       TransportStateReference tmStateReference) {
            // The inbound buffer is wrap(data, 0, length), so only 'remaining' bytes are valid.
            ByteBuffer dup = wholeMessage.duplicate();
            byte[] bytes = new byte[dup.remaining()];
            dup.get(bytes);
            OctetString message = new OctetString(bytes);
            LOGGER.info("Received from " + incomingAddress + ": " + message.toHexString());
            received.add(message);
            if (sync != null) {
                synchronized (sync) {
                    sync.notifyAll();
                }
            }
        }
    }

    private static TransportStateReference newStateReference(TransportMapping<?> tm) {
        return new TransportStateReference(tm, null, null, null, null, false, new Object());
    }

    public void testTransportAuditListenerRegistration() throws Exception {
        DefaultUdpTransportMapping tm = new DefaultUdpTransportMapping(new UdpAddress("127.0.0.1/0"));
        try {
            assertFalse(tm.isTransportAudit());

            RecordingAuditListener listener1 = new RecordingAuditListener(null);
            RecordingAuditListener listener2 = new RecordingAuditListener(null);
            tm.addTransportAuditListener(listener1);
            assertTrue(tm.isTransportAudit());
            // adding the same listener again must not change anything
            tm.addTransportAuditListener(listener1);
            tm.addTransportAuditListener(listener2);
            assertTrue(tm.isTransportAudit());

            tm.removeTransportAuditListener(listener1);
            assertTrue(tm.isTransportAudit());
            tm.removeTransportAuditListener(listener2);
            assertFalse(tm.isTransportAudit());

            tm.addTransportAuditListener(listener1);
            tm.addTransportAuditListener(listener2);
            assertTrue(tm.isTransportAudit());
            tm.removeAllTransportAuditListeners();
            assertFalse(tm.isTransportAudit());
        } finally {
            tm.close();
        }
    }

    public void testListenAndCloseAreAudited() throws Exception {
        DefaultUdpTransportMapping tm = new DefaultUdpTransportMapping(new UdpAddress("127.0.0.1/0"));
        RecordingAuditListener audit = new RecordingAuditListener(null);
        tm.addTransportAuditListener(audit);

        tm.listen();
        assertTrue(tm.isListening());
        UdpAddress listenAddress = tm.getListenAddress();
        assertEquals(1, audit.eventsOf(TransportOperation.LISTEN).size());
        TransportAuditEvent listenEvent = audit.eventsOf(TransportOperation.LISTEN).get(0);
        assertSame(tm, listenEvent.getSource());
        assertEquals(listenAddress, listenEvent.getLocalAddress());
        assertNull(listenEvent.getPeerAddress());
        assertNull(listenEvent.getMessage());

        tm.close();
        assertEquals(1, audit.eventsOf(TransportOperation.CLOSE).size());
        TransportAuditEvent closeEvent = audit.eventsOf(TransportOperation.CLOSE).get(0);
        assertSame(tm, closeEvent.getSource());
        assertEquals(listenAddress, closeEvent.getLocalAddress());
        assertNull(closeEvent.getMessage());
    }

    public void testDeniedListenCancelsListening() throws Exception {
        DefaultUdpTransportMapping tm = new DefaultUdpTransportMapping(new UdpAddress("127.0.0.1/0"));
        RecordingAuditListener audit = new RecordingAuditListener(null, TransportOperation.LISTEN);
        tm.addTransportAuditListener(audit);
        try {
            tm.listen();
            // listening must have been cancelled by the audit listener
            assertFalse(tm.isListening());
            assertNull(tm.getListenWorkerTask());
            assertEquals(1, audit.eventsOf(TransportOperation.LISTEN).size());
        } finally {
            tm.close();
        }
    }

    public void testSendIsAuditedAndDelivered() throws Exception {
        DefaultUdpTransportMapping receiver = new DefaultUdpTransportMapping(new UdpAddress("127.0.0.1/0"));
        DefaultUdpTransportMapping sender = new DefaultUdpTransportMapping(new UdpAddress("127.0.0.1/0"));
        final Object receiveSync = new Object();
        RecordingTransportListener receivedMessages = new RecordingTransportListener(receiveSync);
        receiver.addTransportListener(receivedMessages);
        receiver.listen();
        sender.listen();

        RecordingAuditListener senderAudit = new RecordingAuditListener(null);
        sender.addTransportAuditListener(senderAudit);

        UdpAddress receiverAddress = receiver.getListenAddress();
        synchronized (receiveSync) {
            sender.sendMessage(receiverAddress, PAYLOAD, newStateReference(sender), 0, 0);
            receiveSync.wait(2000);
        }

        // the message was allowed and delivered
        assertEquals(1, receivedMessages.received.size());
        assertEquals(new OctetString(PAYLOAD).toHexString(), receivedMessages.received.get(0).toHexString());

        // and the SEND was audited with the expected details
        List<TransportAuditEvent> sendEvents = senderAudit.eventsOf(TransportOperation.SEND);
        assertEquals(1, sendEvents.size());
        TransportAuditEvent sendEvent = sendEvents.get(0);
        assertSame(sender, sendEvent.getSource());
        assertEquals(receiverAddress, sendEvent.getPeerAddress());
        assertEquals(sender.getListenAddress(), sendEvent.getLocalAddress());
        assertEquals(new OctetString(PAYLOAD).toHexString(),
                new OctetString(sendEvent.getMessage()).toHexString());

        sender.close();
        receiver.close();
    }

    public void testDeniedSendIsNotDelivered() throws Exception {
        DefaultUdpTransportMapping receiver = new DefaultUdpTransportMapping(new UdpAddress("127.0.0.1/0"));
        DefaultUdpTransportMapping sender = new DefaultUdpTransportMapping(new UdpAddress("127.0.0.1/0"));
        final Object receiveSync = new Object();
        RecordingTransportListener receivedMessages = new RecordingTransportListener(receiveSync);
        receiver.addTransportListener(receivedMessages);
        receiver.listen();
        sender.listen();

        RecordingAuditListener senderAudit = new RecordingAuditListener(null, TransportOperation.SEND);
        sender.addTransportAuditListener(senderAudit);

        UdpAddress receiverAddress = receiver.getListenAddress();
        synchronized (receiveSync) {
            sender.sendMessage(receiverAddress, PAYLOAD, newStateReference(sender), 0, 0);
            // give the (denied) message no chance to arrive
            receiveSync.wait(1000);
        }

        // the SEND was audited but cancelled, so nothing was delivered
        assertEquals(1, senderAudit.eventsOf(TransportOperation.SEND).size());
        assertEquals(0, receivedMessages.received.size());

        sender.close();
        receiver.close();
    }

    public void testDeniedReceiveIsNotProcessed() throws Exception {
        DefaultUdpTransportMapping receiver = new DefaultUdpTransportMapping(new UdpAddress("127.0.0.1/0"));
        DefaultUdpTransportMapping sender = new DefaultUdpTransportMapping(new UdpAddress("127.0.0.1/0"));
        RecordingTransportListener receivedMessages = new RecordingTransportListener(null);
        receiver.addTransportListener(receivedMessages);

        final Object auditSync = new Object();
        RecordingAuditListener receiverAudit = new RecordingAuditListener(auditSync, TransportOperation.RECEIVE);
        receiver.addTransportAuditListener(receiverAudit);
        receiver.listen();
        sender.listen();

        UdpAddress receiverAddress = receiver.getListenAddress();
        synchronized (auditSync) {
            sender.sendMessage(receiverAddress, PAYLOAD, newStateReference(sender), 0, 0);
            // wait until the RECEIVE audit event has been fired on the receiver
            auditSync.wait(2000);
        }

        List<TransportAuditEvent> receiveEvents = receiverAudit.eventsOf(TransportOperation.RECEIVE);
        assertEquals(1, receiveEvents.size());
        TransportAuditEvent receiveEvent = receiveEvents.get(0);
        assertSame(receiver, receiveEvent.getSource());
        assertEquals(receiver.getListenAddress(), receiveEvent.getLocalAddress());
        assertEquals(new OctetString(PAYLOAD).toHexString(),
                new OctetString(receiveEvent.getMessage()).toHexString());

        // give the transport listener a chance to be (wrongly) invoked
        Thread.sleep(300);
        // the RECEIVE was denied, so the message must not have been processed
        assertEquals(0, receivedMessages.received.size());

        sender.close();
        receiver.close();
    }
}
