/*_############################################################################
  _## 
  _##  SNMP4J-Agent 3 - TestDefaultMOServer.java  
  _## 
  _##  Copyright (C) 2005-2025  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.agent;

import junit.framework.*;
import org.snmp4j.agent.mo.GenericManagedObject;
import org.snmp4j.agent.mo.lock.LockRequest;
import org.snmp4j.agent.mo.lock.MOLockStrategy;
import org.snmp4j.agent.request.SnmpRequest;
import org.snmp4j.agent.request.SnmpSubRequest;
import org.snmp4j.agent.request.SubRequest;
import org.snmp4j.log.ConsoleLogFactory;
import org.snmp4j.log.LogAdapter;
import org.snmp4j.log.LogFactory;
import org.snmp4j.log.LogLevel;
import org.snmp4j.smi.*;
import org.snmp4j.agent.mo.MOScalar;
import org.snmp4j.mp.SnmpConstants;
import org.snmp4j.agent.mo.MOAccessImpl;

import java.util.Stack;

import static org.junit.Assert.assertEquals;

public class TestDefaultMOServer extends TestCase {

    static {
        LogFactory.setLogFactory(new ConsoleLogFactory());
        LogFactory.getLogFactory().getRootLogger().setLogLevel(LogLevel.WARN);
    }

    private static final LogAdapter logger = LogFactory.getLogger(TestDefaultMOServer.class);

    private static final OID TEST_ENT_OID_LOW = new OID("1.3.6.1.4.1.777777.80.2.8888.1.1.1.2.1");
    private static final OID TEST_ENT_OID_UP = new OID("1.3.6.1.4.1.777777.80.2.8888.1.1.1.2.4");
    private static final OID TEST_ENT_OID_INST = new OID("1.3.6.1.4.1.777777.80.2.8888.1.1.1.2.1.0");
    private static final OID TEST_ENT_OID_INST2 = new OID("1.3.6.1.4.1.777777.80.2.8888.1.1.1.2.2.0");
    private static final OID TEST_ENT_OID_LOW2 = new OID("1.3.6.1.4.1.777777.80.2.8888.1.1.1.2.2");
    private static final OID TEST_ENT_OID_UP2 = new OID("1.3.6.1.4.1.777777.80.2.8888.1.1.1.2.5");

    private DefaultMOServer defaultMOServer = null;
    private MOContextScope tableScope1 =
            new DefaultMOContextScope(new OctetString(), TEST_ENT_OID_LOW, true, TEST_ENT_OID_UP, false);
    private MOContextScope tableScope2 =
            new DefaultMOContextScope(new OctetString("context2"), TEST_ENT_OID_LOW2, true, TEST_ENT_OID_UP2, false);
    private TestRange table = new TestRange(tableScope1);
    private TestRange table2 = new TestRange(tableScope2);

    private static class LockTestMOScalar<V extends Variable> extends MOScalar<V> {
        private Stack<LockRequest> lockRequestStack = new Stack<>();

        public LockTestMOScalar(OID id, MOAccess access, V value) {
            super(id, access, value);
        }
        public void lockBegin(LockRequest lockRequest) {
            if (logger.isDebugEnabled()) {
                logger.debug("Lock begin "+lockRequest);
            }
            lockRequestStack.push(lockRequest);
        }

        public void lockEnd(LockRequest lockRequest) {
            LockRequest active = lockRequestStack.pop();
            if (logger.isDebugEnabled()) {
                logger.debug("Lock end "+lockRequest);
            }
            if (active != lockRequest) {
                System.err.println(lockRequestStack);
            }
            assertEquals(active, lockRequest);
        }
    }


    public TestDefaultMOServer(String name) {
        super(name);
    }

    protected void setUp() throws Exception {
        super.setUp();
        defaultMOServer = new DefaultMOServer();
        defaultMOServer.register(table, new OctetString());
        defaultMOServer.register(new MOScalar<OctetString>(SnmpConstants.sysDescr,
                        MOAccessImpl.ACCESS_READ_WRITE,
                        new OctetString("contextDefault")),
                new OctetString());
        defaultMOServer.register(new MOScalar<>(SnmpConstants.sysName,
                        MOAccessImpl.ACCESS_READ_WRITE,
                        new OctetString("contextDefault")),
                new OctetString());
        defaultMOServer.register(new LockTestMOScalar<Integer32>(SnmpConstants.sysServices,
                        MOAccessImpl.ACCESS_READ_ONLY,
                        new Integer32(0)),
                new OctetString());
        defaultMOServer.register(new MOScalar<TimeTicks>(SnmpConstants.sysUpTime,
                        MOAccessImpl.ACCESS_READ_ONLY,
                        new TimeTicks(0)),
                new OctetString());
        defaultMOServer.register(new MOScalar<OctetString>(SnmpConstants.sysContact,
                        MOAccessImpl.ACCESS_READ_WRITE,
                        new OctetString("Sys-Admin")),
                new OctetString());
        defaultMOServer.register(new MOScalar<OctetString>(SnmpConstants.sysDescr,
                        MOAccessImpl.ACCESS_READ_WRITE,
                        new OctetString("context1")),
                new OctetString("context1"));
        defaultMOServer.register(new MOScalar<OctetString>(SnmpConstants.sysDescr,
                        MOAccessImpl.ACCESS_READ_WRITE,
                        new OctetString("context2")),
                new OctetString("context2"));

        defaultMOServer.register(table2, new OctetString("context2"));
    }

    protected void tearDown() throws Exception {
        defaultMOServer = null;
        super.tearDown();
    }

    public void testGetManagedObject() {
        OID key = SnmpConstants.sysDescr;
        OctetString context = new OctetString();
        Variable expectedReturn = new OctetString("contextDefault");
        ManagedObject<?> actualReturn = defaultMOServer.getManagedObject(key, context);
        assertNotNull(actualReturn);
        assertEquals("Result", expectedReturn, ((MOScalar<?>) actualReturn).getValue());
        context = new OctetString("context2");
        expectedReturn = new OctetString("context2");
        actualReturn = defaultMOServer.getManagedObject(key, context);
        assertNotNull(actualReturn);
        assertEquals("Result", expectedReturn, ((MOScalar<?>) actualReturn).getValue());
    }

    public void testLookup() {
        OctetString context = new OctetString();
        MOQuery query = new DefaultMOQuery(
                new DefaultMOContextScope(context,
                        SnmpConstants.sysDescr, true,
                        SnmpConstants.sysName, true));
        Variable expectedReturn = new OctetString("contextDefault");
        ManagedObject<?> actualReturn = defaultMOServer.lookup(query);
        assertEquals("Result", expectedReturn, ((MOScalar<?>) actualReturn).getValue());
        context = new OctetString("context2");
        query = new DefaultMOQuery(
                new DefaultMOContextScope(context,
                        SnmpConstants.sysDescr, true,
                        SnmpConstants.sysName, true));
        actualReturn = defaultMOServer.lookup(query);
        assertEquals("Result2", context, ((MOScalar<?>) actualReturn).getValue());

        query = new DefaultMOQuery(
                new DefaultMOContextScope(new OctetString(), TEST_ENT_OID_INST, true, null, false));
        actualReturn = defaultMOServer.lookup(query);
        assertNotNull(actualReturn);
        assertEquals("Result3", table, actualReturn);
        query = new DefaultMOQuery(
                new DefaultMOContextScope(new OctetString("context2"), TEST_ENT_OID_INST2, true, null, false));
        actualReturn = defaultMOServer.lookup(query);
        assertNotNull(actualReturn);
        assertEquals("Result4", table2, actualReturn);
    }

    public void testConcurrentLookupWithLocking() throws InterruptedException {
        defaultMOServer.setLockStrategy((managedObjectLookedUp, query) -> true);
        UpdaterThread[] updaters = new UpdaterThread[25];
        for (int i = 0; i < updaters.length; i++) {
            updaters[i] = new UpdaterThread(500, 1000, 0);
            updaters[i].start();
        }
        for (UpdaterThread updater : updaters) {
            updater.join();
        }
    }

    public void _testRegister() throws DuplicateRegistrationException {
        ManagedObject<SubRequest<?>> mo = null;
        OctetString context = null;
        defaultMOServer.register(mo, context);

    }

    public void testUnregister() {
        OctetString context = new OctetString();
        ManagedObject<?> mo = new MOScalar<>(SnmpConstants.sysDescr,
                MOAccessImpl.ACCESS_READ_WRITE,
                context);
        defaultMOServer.unregister(mo, null);
        assertNotNull(defaultMOServer.getManagedObject(SnmpConstants.sysDescr, context));
        ManagedObject<?> origMO = defaultMOServer.getManagedObject(SnmpConstants.sysDescr, context);
        assertNotNull(origMO);
        assertEquals(SnmpConstants.sysDescr.trim(), origMO.getScope().getLowerBound());
        defaultMOServer.unregister(origMO, context);
        assertNull(defaultMOServer.getManagedObject(SnmpConstants.sysDescr, context));
    }

    public void testDeadlock4Threads() {
        defaultMOServer.setLockStrategy((managedObjectLookedUp, query) -> true);
        defaultMOServer.setDeadlockPreventionEnabled(true);
        for (int i=0; i<100; i++) {
            OctetString context = new OctetString();
            MOQuery querySysServices = new DefaultMOQuery(
                    new DefaultMOContextScope(context,
                            SnmpConstants.sysServices, true,
                            SnmpConstants.sysServices, true));
            MOQuery querySysName = new DefaultMOQuery(
                    new DefaultMOContextScope(context,
                            SnmpConstants.sysName, true,
                            SnmpConstants.sysName, true));
            MOQuery querySysDescr = new DefaultMOQuery(
                    new DefaultMOContextScope(context,
                            SnmpConstants.sysDescr, true,
                            SnmpConstants.sysDescr, true));
            Object thread1 = "1";
            Object thread2 = "2";
            Object thread3 = "3";
            Object thread4 = "4";

            LockRequest lockRequestThread1 = new LockRequest(thread1, 10000);
            LockRequest lockRequestThread2 = new LockRequest(thread2, 10000);
            LockRequest lockRequestThread3 = new LockRequest(thread3, 10000);
            LockRequest lockRequestThread4 = new LockRequest(thread4, 10000);
            MOServerLookupEvent lookupEventSysServices = new MOServerLookupEvent(this, null,
                    querySysServices, MOServerLookupEvent.IntendedUse.update, true);
            MOServerLookupEvent lookupEventSysDescr = new MOServerLookupEvent(this, null,
                    querySysDescr, MOServerLookupEvent.IntendedUse.update, true);
            MOServerLookupEvent lookupEventSysName = new MOServerLookupEvent(this, null,
                    querySysName, MOServerLookupEvent.IntendedUse.update, true);

            ManagedObject<?> moSysDescr = defaultMOServer.lookup(querySysDescr, lockRequestThread1, lookupEventSysDescr);
            assertNotNull(moSysDescr);
            ManagedObject<?> moSysServices = defaultMOServer.lookup(querySysServices, lockRequestThread2, lookupEventSysServices);
            assertNotNull(moSysServices);

            Thread lookupLockedThread2 = new Thread(null, () -> {
                ManagedObject<?> moSysDescr2 = defaultMOServer.lookup(querySysDescr, lockRequestThread2, lookupEventSysDescr);
                ((LockTestMOScalar<?>) moSysServices).lockBegin(lockRequestThread2);
                assertNotNull(moSysDescr2);
                assertEquals(LockRequest.LockStatus.locked, lockRequestThread2.getLockRequestStatus());
                defaultMOServer.unlock(thread2, moSysDescr2);
                ((LockTestMOScalar<?>) moSysServices).lockEnd(lockRequestThread2);
                defaultMOServer.unlock(thread2, moSysServices);
                logger.debug("Thread 2 finished");
            }, "Thread 2");
            Thread lookupLockedThread1 = new Thread(null, () -> {
                ManagedObject<?> moSysServices2 = defaultMOServer.lookup(querySysServices, lockRequestThread1, lookupEventSysServices);
                assertNotNull(moSysServices2);
                assertEquals(LockRequest.LockStatus.locked, lockRequestThread1.getLockRequestStatus());
                ((LockTestMOScalar<?>) moSysServices2).lockBegin(lockRequestThread1);
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                ((LockTestMOScalar<?>) moSysServices2).lockEnd(lockRequestThread1);
                defaultMOServer.unlock(thread1, moSysServices2);
                defaultMOServer.unlock(thread1, moSysDescr);
                logger.debug("Thread 1 finished");
            }, "Thread 1");
            Thread lookupLockedThread3 = new Thread(null, () -> {
                ManagedObject<?> moSysServices3 = defaultMOServer.lookup(querySysServices, lockRequestThread3, lookupEventSysServices);
                assertNotNull(moSysServices3);
                assertEquals(LockRequest.LockStatus.locked, lockRequestThread3.getLockRequestStatus());
                ManagedObject<?> moSysDescr3 = defaultMOServer.lookup(querySysDescr, lockRequestThread3, lookupEventSysDescr);
                assertNotNull(moSysDescr3);
                assertEquals(LockRequest.LockStatus.locked, lockRequestThread3.getLockRequestStatus());
                ((LockTestMOScalar<?>) moSysServices3).lockBegin(lockRequestThread3);
                try {
                    Thread.sleep(2);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                ((LockTestMOScalar<?>) moSysServices3).lockEnd(lockRequestThread3);
                defaultMOServer.unlock(thread3, moSysServices3);
                defaultMOServer.unlock(thread3, moSysDescr3);
                logger.debug("Thread 3 finished");
            }, "Thread 3");
            Thread lookupLockedThread4 = new Thread(null, () -> {
                ManagedObject<?> moSysServices4 = defaultMOServer.lookup(querySysServices, lockRequestThread4, lookupEventSysServices);
                assertNotNull(moSysServices4);
                assertEquals(LockRequest.LockStatus.locked, lockRequestThread4.getLockRequestStatus());
                ((LockTestMOScalar<?>) moSysServices4).lockBegin(lockRequestThread4);
                try {
                    Thread.sleep(7);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                ((LockTestMOScalar<?>) moSysServices4).lockEnd(lockRequestThread4);
                defaultMOServer.unlock(thread4, moSysServices4);
                logger.debug("Thread 4 finished");
            }, "Thread 4");
            lookupLockedThread1.start();
            lookupLockedThread3.start();
            lookupLockedThread2.start();
            lookupLockedThread4.start();
            try {
                lookupLockedThread2.join();
                lookupLockedThread1.join();
                lookupLockedThread3.join();
                lookupLockedThread4.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            assertEquals(0, defaultMOServer.getWaitingLockRequests().size());
            assertEquals(0, defaultMOServer.getActiveLockRequests().size());
        }
    }

    public void testDeadlock2Threads5MO() {
        defaultMOServer.setLockStrategy((managedObjectLookedUp, query) -> true);
        defaultMOServer.setDeadlockPreventionEnabled(true);
        OctetString context = new OctetString();
        MOQuery[] queries = new MOQuery[] {
            new DefaultMOQuery(
                new DefaultMOContextScope(context,
                        SnmpConstants.sysServices, true,
                        SnmpConstants.sysServices, true)),
            new DefaultMOQuery(
                    new DefaultMOContextScope(context,
                            SnmpConstants.sysName, true,
                            SnmpConstants.sysName, true)),
            new DefaultMOQuery(
                    new DefaultMOContextScope(context,
                            SnmpConstants.sysUpTime, true,
                            SnmpConstants.sysUpTime, true)),
            new DefaultMOQuery(
                    new DefaultMOContextScope(context,
                            SnmpConstants.sysContact, true,
                            SnmpConstants.sysContact, true)),
            new DefaultMOQuery(
                new DefaultMOContextScope(context,
                        SnmpConstants.sysDescr, true,
                        SnmpConstants.sysDescr, true))
        };
        Object[] threads = new Object[] { new Object(), new Object() };

        LockRequest[] lockRequests = new LockRequest[] {
                new LockRequest(threads[0], 10000),
                new LockRequest(threads[1], 10000),
        };
        MOServerLookupEvent[] lookupEvent = new MOServerLookupEvent[queries.length];
        for (int i = 0; i<queries.length; i++) {
            lookupEvent[i] = new MOServerLookupEvent(this, null, queries[i], MOServerLookupEvent.IntendedUse.update, true);
        }

        ManagedObject<?> mo5 = defaultMOServer.lookup(queries[4], lockRequests[0], lookupEvent[4]);
        assertNotNull(mo5);
        ManagedObject<?>[] mo1_4 = new ManagedObject[4];
        for (int i=0; i<4; i++) {
            mo1_4[i] = defaultMOServer.lookup(queries[i], lockRequests[1], lookupEvent[i]);
            assertNotNull(mo1_4[i]);
        }

        Thread lookupLockedThread1 = new Thread(null, () -> {
            ManagedObject<?> mo4_t2 = defaultMOServer.lookup(queries[3], lockRequests[0], lookupEvent[3]);
            assertNotNull(mo4_t2);
            defaultMOServer.unlock(threads[0], mo4_t2);
            defaultMOServer.unlock(threads[0], mo5);
            logger.debug("Thread 1 finished");
        }, "Thread 1");
        Thread lookupLockedThread2 = new Thread(null, () -> {
            ManagedObject<?> mo5_t2 = defaultMOServer.lookup(queries[4], lockRequests[1], lookupEvent[4]);
            assertNotNull(mo5_t2);
            defaultMOServer.unlock(threads[1], mo5_t2);
            for (int i=0; i<4; i++) {
                defaultMOServer.unlock(threads[1], mo1_4[i]);
            }
            logger.debug("Thread 2 finished");
        }, "Thread 2");
        lookupLockedThread1.start();
        lookupLockedThread2.start();
        try {
            lookupLockedThread2.join();
            lookupLockedThread1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        assertEquals(0, defaultMOServer.getWaitingLockRequests().size());
        assertEquals(0, defaultMOServer.getActiveLockRequests().size());
    }

    class TestRange implements GenericManagedObject {
        private MOContextScope tableScope;

        public TestRange(MOContextScope scope) {
            this.tableScope = scope;
        }

        @Override
        public MOScope getScope() {
            return tableScope;
        }

        @Override
        public OID find(MOScope range) {
            OID lowerBound = range.getLowerBound();
            OID next = OID.max(((lowerBound == null) ? tableScope.getLowerBound() : lowerBound), tableScope.getLowerBound());
            if (tableScope.covers(next)) {
                return next;
            }
            return null;
        }

        @Override
        public void get(SubRequest<?> request) {
        }

        @Override
        public boolean next(SubRequest<?> request) {
            return false;
        }

        @Override
        public void prepare(SubRequest<?> request) {
        }

        @Override
        public void commit(SubRequest<?> request) {
        }

        @Override
        public void undo(SubRequest<?> request) {
        }

        @Override
        public void cleanup(SubRequest<?> request) {
        }

    }

    private class UpdaterThread extends Thread {
        private long waitMillis = 10;
        private long timeoutMillis = 500;
        private int updateCount = 1000;
        private boolean updateOK = false;

        public UpdaterThread(long timeoutMillis, int updateCount, long waitMillis) {
            this.timeoutMillis = timeoutMillis;
            this.updateCount = updateCount;
            this.waitMillis = waitMillis;
        }

        @Override
        @SuppressWarnings("unchecked")
        public void run() {
            OctetString context = new OctetString();
            MOQuery query = new DefaultMOQuery(
                    new DefaultMOContextScope(context,
                            SnmpConstants.sysServices, true,
                            SnmpConstants.sysServices, true));
            LockRequest lockRequest = new LockRequest(this, timeoutMillis);
            int startValue = -1;
            for (int i = 0; i < updateCount; i++) {
                MOServerLookupEvent lookupEvent = new MOServerLookupEvent(this, null,
                        query, MOServerLookupEvent.IntendedUse.update, true);

                ManagedObject<?> mo = defaultMOServer.lookup(query, lockRequest, lookupEvent);
                if (mo != null) {
                    assertEquals(LockTestMOScalar.class, mo.getClass());
                    assertEquals(LockRequest.LockStatus.locked, lockRequest.getLockRequestStatus());
                    int currentValue = ((MOScalar<?>) mo).getValue().toInt();
                    if (startValue < 0) {
                        startValue = currentValue;
                    }
                    //noinspection unchecked
                    ((MOScalar<Integer32>) mo).setValue(new Integer32(++currentValue));
                    lookupEvent.completedUse(mo);
                    if (waitMillis > 0) {
                        try {
                            sleep(waitMillis);
                        } catch (InterruptedException iex) {
                            // ignore
                        }
                    }
                    int currentValueAfterWait = ((MOScalar<?>) mo).getValue().toInt();
                    boolean unlocked = defaultMOServer.unlock(lockRequest.getLockOwner(), mo);
                    assertTrue(unlocked);
                    assertEquals(currentValue, currentValueAfterWait);
                    assertTrue(currentValueAfterWait > startValue + i);
                } else {
                    assertEquals(LockRequest.LockStatus.lockTimedOut, lockRequest.getLockRequestStatus());
                }
            }
            updateOK = true;
        }
    }
}
