package net.i2p.router.transport;
/*
 * free (adj.): unencumbered; not under the control of others
 * Written by jrandom in 2003 and released into the public domain
 * with no warranty of any kind, either expressed or implied.
 * It probably won't make your computer catch on fire, or eat
 * your children, but it might.  Use at your own risk.
 *
 */

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Properties;
import java.util.Random;

import org.junit.BeforeClass;
import org.junit.Test;

import static junit.framework.TestCase.*;
import net.i2p.router.Router;
import net.i2p.router.RouterContext;

/**
 * Stress out the bandwidth limiter by running a series of push and pull tests
 * through bandwidth limited streams.  This includes pushing data through 
 * unthrottled streams, through streams throttled at 4KBps, 32KBps, and 256KBps, 
 * pulling data through those same rates, as well as doing so with 10 concurrent
 * threads (and, in turn, 10 concurrent streams all using the same BandwidthLimiter).
 *
 * Note: this takes a long time to run (~1 hour) since the 4KBps push/pull of 1MB with
 * 10 concurrent threads is, well, slow.
 *
 */
public class BandwidthLimiterTest {
    private static RouterContext _context;
    private final static int NUM_KB = 256;
    
    @BeforeClass
    public static void setUp() {
        _context = new RouterContext(new Router());
        _context.initAll();
    }
    
    private void prepareLimiter(int inKBps, int outKBps, int inBurst, int outBurst) {
        Properties props = System.getProperties();
        props.setProperty(FIFOBandwidthRefiller.PROP_INBOUND_BANDWIDTH, ""+inKBps);
        props.setProperty(FIFOBandwidthRefiller.PROP_OUTBOUND_BANDWIDTH, ""+outKBps);
        props.setProperty(FIFOBandwidthRefiller.PROP_INBOUND_BANDWIDTH_PEAK, ""+inBurst);
        props.setProperty(FIFOBandwidthRefiller.PROP_OUTBOUND_BANDWIDTH_PEAK, ""+outBurst);
        //props.setProperty(TrivialBandwidthLimiter.PROP_REPLENISH_FREQUENCY, ""+10*1000);
        System.setProperties(props);
        _context.bandwidthLimiter().reinitialize();
    }
    
    /**
     * Using the configured limiter, determine how long it takes to shove 
     * numBytes through a BandwidthLimitedOutputStream (broken up into numBytesPerWrite)
     * chunks.
     *
     */
    private long testOutboundThrottle(int numBytes, int numBytesPerWrite) {
        byte source[] = new byte[numBytesPerWrite];
        new Random().nextBytes(source);
        NullOutputStream target = new NullOutputStream();
        BandwidthLimitedOutputStream out = new BandwidthLimitedOutputStream(_context, target, null);
        long before = System.currentTimeMillis();
        try {
            for (int i = 0; i < numBytes; i += numBytesPerWrite) {
                int num = numBytesPerWrite;
                if (numBytesPerWrite + i >= numBytes)
                    num = numBytes - i;
                //_log.info("** Writing " + num + " bytes starting at " + i);
                out.write(source, 0, num);
            }
        } catch (IOException ioe) {}
        long after = System.currentTimeMillis();
        return after-before;
    }
    
    /**
     * Using the configured limiter, determine how long it takes to read 
     * numBytes through a BandwidthLimitedInputStream (broken up into numBytesPerRead)
     * chunks.
     *
     */
    private long testInboundThrottle(int numBytes, int numBytesPerRead) {
        FakeInputStream source = new FakeInputStream(numBytes);
        BandwidthLimitedInputStream in = new BandwidthLimitedInputStream(_context, source, null);
        long before = System.currentTimeMillis();
        try {
            byte buf[] = new byte[numBytesPerRead];
            int read = 0;
            while ( (read = in.read(buf)) != -1) {
                //_log.info("** Read " + read + " bytes");
                // gobble the data.  who cares
            }
        } catch (IOException ioe) {}
        long after = System.currentTimeMillis();
        return after-before;
    }
    
    /**
     * Run a series of tests on outbound throttling (shoving lots of data through pipes 
     * with various limits) and log the times.
     *
     */
    @Test
    public void testOutbound() {
    	double error;
    	double predict;
    	
        prepareLimiter(-1, -1, -1, -1);
        long ms = testOutboundThrottle(NUM_KB*1024, 1*1024);
        
        /*prepareLimiter(-1, 4, -1, 4*1024);
        ms = testOutboundThrottle(NUM_KB*1024, 1*1024);
        predict = (NUM_KB/4)*1000;
        error = predict/ms;
        //assertTrue(error>.89);
        assertTrue(error<1.05);*/
        
        prepareLimiter(-1, 32, -1, 32*1024);
        ms = testOutboundThrottle(NUM_KB*1024, 1*1024);
        predict = (NUM_KB/32)*1000;
        error = predict/ms;
        //assertTrue(error>.89);
        assertTrue(error<1.05);
        
        prepareLimiter(-1, 256, -1, 256*1024);
        ms = testOutboundThrottle(NUM_KB*1024, 1*1024);
        predict = (NUM_KB/256)*1000;
        error = predict/ms;
        //assertTrue(error>.89);
        assertTrue(error<1.05);
        
    }
    
    /**
     * Run a series of tests on inbound throttling (pulling lots of data through pipes 
     * with various limits) and log the times.
     *
     */
    @Test
    public void testInbound() {
        double predict;
        double error;
    	
        prepareLimiter(-1, -1, -1, -1);
        long ms = testInboundThrottle(NUM_KB*1024, 1*1024);
        
        /*prepareLimiter(4, -1, 4*1024, -1);
        ms = testInboundThrottle(NUM_KB*1024, 1*1024);
        predict = (NUM_KB/4)*1000;
        error = predict/ms;
        //assertTrue(error>.89);
        assertTrue(error<1.05);*/
        
        prepareLimiter(32, -1, 32*1024, -1);
        ms = testInboundThrottle(NUM_KB*1024, 1*1024);
        predict = (NUM_KB/32)*1000;
        error = predict/ms;
        //assertTrue(error>.89);
        assertTrue(error<1.05);
        
        prepareLimiter(256, -1, 256*1024, -1);
        ms = testInboundThrottle(NUM_KB*1024, 1*1024);
        predict = (NUM_KB/256)*1000;
        error = predict/ms;
        //assertTrue(error>.89);
        assertTrue(error<1.05);
        
    }
    
    
    @Test
    public void testOutboundContention() {
    	double predict;
    	double error;
    	long ms;
    	long end;
    	long start;
    	
        prepareLimiter(-1, -1, -1, -1);
        start = System.currentTimeMillis();
        //long runningTimes[] = testOutboundContention(10, NUM_KB*1024);
        end = System.currentTimeMillis();
        
        //prepareLimiter(-1, 4, -1, 5*1024*1024);
        //start = System.currentTimeMillis();
        //runningTimes = testOutboundContention(10, NUM_KB*1024);
        //end = System.currentTimeMillis();
        
        //prepareLimiter(-1, 32, -1, 32*1024);
        //start = System.currentTimeMillis();
        //runningTimes = testOutboundContention(10, NUM_KB*1024);
        //end = System.currentTimeMillis();
        
        prepareLimiter(-1, 256, -1, 256*1024);
        start = System.currentTimeMillis();
        testOutboundContention(10, NUM_KB*1024);
        end = System.currentTimeMillis();
        ms = end-start;
        predict = (NUM_KB/256)*1000*10;
        error = predict/ms;
        //assertTrue(error>.89);
        assertTrue(error<1.05);
        
    }
    
    private long[] testOutboundContention(int numConcurrent, int numBytes) {
        OutboundRunner threads[] = new OutboundRunner[numConcurrent];
        for (int i = 0; i < numConcurrent; i++) {
            threads[i] = new OutboundRunner(numBytes);
        }
        
        for (int i = 0; i < numConcurrent; i++)
            threads[i].start();
        for (int i = 0; i < numConcurrent; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException ie) {}
        }
        long rv[] = new long[numConcurrent];
        for (int i = 0; i < numConcurrent; i++)
            rv[i] = threads[i].getRunningTime();
        return rv;
    }

    private static int __runnerNum = 0;
    private class OutboundRunner extends Thread {
        private int _numBytes;
        private int _runnerNum;
        private long _runningTime;
        public OutboundRunner(int numBytes) {
            _numBytes = numBytes;
            _runnerNum = ++__runnerNum;
        }
        public void run() {
            Thread.currentThread().setName("Out" + _runnerNum);
            _runningTime = testOutboundThrottle(_numBytes, 8*1024);
        }
        public long getRunningTime() { return _runningTime; }
    }
}

class NullOutputStream extends OutputStream {
    public void write(int param) {}
}

class FakeInputStream extends InputStream {
    private volatile int _numRead;
    private int _size;
    
    public FakeInputStream(int size) {
        _size = size;
        _numRead = 0;
    }
    public int read() {
        int rv = 0;
        if (_numRead >= _size) 
            rv = -1;
        else
            rv = 42;
        _numRead++;
        return rv;
    }
}