package freenet.node;

import static org.junit.Assert.*;

import java.util.HashSet;
import java.util.Set;

import org.junit.Test;

public class PeerLocationTest {
    private static final double EPSILON = 1e-15;

    private static final double[][] PEER_LOCATIONS = new double[][] {
        // Must be sorted!
        { 0.1 },
        { 0.0 },
        { 0.9 },
        { 0.1, 0.3 },
        { 0.1, 0.3, 0.4 },
        { 0.0, 0.1, 0.2, 0.3, 0.3, 0.35, 0.5, 0.5, 0.99 },
        { 0.1, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.20, 0.21, 0.22, 0.23, 0.24 },
        { 0.0000001, 0.0000003, 0.0001, 0.0002, 0.4, 0.5, 0.999, 0.999999, 0.9999995 },
        { 0, 0.1, 0.11, 0.9, 0.99, 1 }
    };

    private static final double[] TARGET_LOCATIONS = new double[] {
        0.0, 1e-12, 0.01, 0.05, 0.09, 0.1, 0.11, 0.29, 0.3, 0.31,
        0.35, 0.4, 0.45, 0.5, 0.51, 0.9, 0.91, 0.9999, 1 - 1e-12
    };

    @Test
    public void testFindClosestLocation() {
        for (double[] peers : PEER_LOCATIONS) {
            for (double target : TARGET_LOCATIONS) {
                int closest = PeerLocation.findClosestLocation(peers, target);
                double ref = trivialFindClosestDistance(peers, target);
                assertEquals(ref, Location.distance(peers[closest], target), EPSILON);
            }
        }
    }

    @Test
    public void testGetClosestPeerLocation() {
        for (double[] peers : PEER_LOCATIONS) {
            PeerLocation pl = new PeerLocation("0.0");
            assertTrue(pl.updateLocation(0.0, peers));
            for (double target : TARGET_LOCATIONS) {
                for (Set<Double> exclude : omit(peers)) {
                    double closest = pl.getClosestPeerLocation(target, exclude);
                    assertFalse(exclude.contains(closest));
                    double ref = trivialFindClosestDistance(peers, target, exclude);
                    if (!Double.isInfinite(ref)) {
                        assertFalse(Double.isNaN(closest));
                        boolean isPeer = false;
                        for (double peer : peers) {
                            if (closest == peer) {
                                isPeer = true;
                                break;
                            }
                        }
                        assertTrue(isPeer);
                        assertEquals(ref, Location.distance(closest, target), EPSILON);
                    } else {
                        assertTrue(Double.isNaN(closest));
                    }
                }
            }
        }
    }

    // Generate sets of sequential omitted values of half the length, plus the empty set
    @SuppressWarnings("unchecked")
    private Set<Double>[] omit(double[] locs) {
        Set<Double>[] result = (Set<Double>[]) new Set<?>[locs.length + 1];
        result[locs.length] = new HashSet<Double>();
        int n = locs.length / 2 + 1;
        for (int i = 0; i < locs.length; i++) {
            Set<Double> s = new HashSet<Double>();
            for (int j = 0; j < n; j++) {
                s.add(locs[(i + j) % locs.length]);
            }
            result[i] = s;
            assertFalse(result[i].isEmpty());
        }
        return result;
    }

    // Trivial reference implementation that finds the distance to the closest location
    private double trivialFindClosestDistance(double[] locs, double l) {
        double minDist = Double.POSITIVE_INFINITY;
        for (int i = 0; i < locs.length; i++) {
            final double d = Location.distance(locs[i], l);
            if (d < minDist) {
                minDist = d;
            }
        }
        return minDist;
    }

    // Trivial reference implementation that finds the distance to the closest location, with some
    // locations excluded from consideration
    private double trivialFindClosestDistance(double[] locs, double l, Set<Double> exclude) {
        double minDist = Double.POSITIVE_INFINITY;
        for (int i = 0; i < locs.length; i++) {
            if (exclude.contains(locs[i])) {
                continue;
            }
            final double d = Location.distance(locs[i], l);
            if (d < minDist) {
                minDist = d;
            }
        }
        return minDist;
    }
}

