/*******************************************************************************
 * Copyright (c) 2011, 2013 VMware Inc.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    SpringSource, a division of VMware - initial API and implementation and/or initial documentation
 *******************************************************************************/

package org.eclipse.equinox.region.internal.tests;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.*;
import org.eclipse.equinox.region.*;
import org.eclipse.equinox.region.RegionDigraph.FilteredRegion;
import org.eclipse.virgo.teststubs.osgi.framework.StubBundle;
import org.eclipse.virgo.teststubs.osgi.framework.StubBundleContext;
import org.junit.Before;
import org.junit.Test;
import org.osgi.framework.*;

public class StandardRegionDigraphTests {

	private RegionDigraph digraph;

	private Region mockRegion1;

	private Region mockRegion2;

	private Region mockRegion3;

	private RegionFilter regionFilter1;

	private RegionFilter regionFilter2;

	@Before
	public void setUp() throws Exception {
		StubBundle stubSystemBundle = new StubBundle(0L, "osgi.framework", new Version("0"), "loc");
		StubBundleContext stubBundleContext = new StubBundleContext();
		stubBundleContext.addInstalledBundle(stubSystemBundle);
		this.digraph = RegionReflectionUtils.newStandardRegionDigraph(stubBundleContext, new ThreadLocal<Region>());

		this.mockRegion1 = mock(Region.class);
		when(this.mockRegion1.getName()).thenReturn("mockRegion1");
		when(this.mockRegion1.getRegionDigraph()).thenReturn(this.digraph);

		this.mockRegion2 = mock(Region.class);
		when(this.mockRegion2.getName()).thenReturn("mockRegion2");
		when(this.mockRegion2.getRegionDigraph()).thenReturn(this.digraph);

		this.mockRegion3 = mock(Region.class);
		when(this.mockRegion3.getName()).thenReturn("mockRegion3");
		when(this.mockRegion3.getRegionDigraph()).thenReturn(this.digraph);

	}

	private void setDefaultFilters() {
		this.regionFilter1 = digraph.createRegionFilterBuilder().build();
		this.regionFilter2 = digraph.createRegionFilterBuilder().build();
	}

	private void setAllowedFilters(String b1Name, Version b1Version, String b2Name, Version b2Version)
			throws InvalidSyntaxException {
		String filter1 = "(&(" + RegionFilter.VISIBLE_BUNDLE_NAMESPACE + "=" + b1Name + ")("
				+ Constants.BUNDLE_VERSION_ATTRIBUTE + "=" + b1Version + "))";
		regionFilter1 = digraph.createRegionFilterBuilder().allow(RegionFilter.VISIBLE_BUNDLE_NAMESPACE, filter1)
				.build();

		String filter2 = "(&(" + RegionFilter.VISIBLE_BUNDLE_NAMESPACE + "=" + b2Name + ")("
				+ Constants.BUNDLE_VERSION_ATTRIBUTE + "=" + b2Version + "))";
		regionFilter2 = digraph.createRegionFilterBuilder().allow(RegionFilter.VISIBLE_BUNDLE_NAMESPACE, filter2)
				.build();

	}

	@Test
	public void testConnect() throws BundleException {
		setDefaultFilters();

		this.digraph.connect(this.mockRegion1, this.regionFilter1, this.mockRegion2);
	}

	@Test
	public void testConnectWithFilterContents() throws BundleException, InvalidSyntaxException {
		String b1Name = "b1";
		Version b1Version = new Version("0");
		String b2Name = "b2";
		Version b2Version = new Version("0");
		setAllowedFilters(b1Name, b1Version, b2Name, b2Version);
		when(this.mockRegion1.getBundle(b1Name, b1Version)).thenReturn(null);
		when(this.mockRegion1.getBundle(b2Name, b2Version)).thenReturn(null);

		this.digraph.connect(this.mockRegion1, this.regionFilter1, this.mockRegion2);
		this.digraph.connect(this.mockRegion1, this.regionFilter2, this.mockRegion3);
	}

	@Test(expected = BundleException.class)
	public void testConnectLoop() throws BundleException {
		setDefaultFilters();

		this.digraph.connect(this.mockRegion1, this.regionFilter1, this.mockRegion1);
	}

	@Test(expected = BundleException.class)
	public void testDuplicateConnection() throws BundleException {
		setDefaultFilters();

		this.digraph.connect(this.mockRegion1, this.regionFilter1, this.mockRegion2);
		this.digraph.connect(this.mockRegion1, this.regionFilter2, this.mockRegion2);
	}

	@Test
	public void testReplaceConnection() throws BundleException {
		setDefaultFilters();

		RegionFilter existing;

		existing = this.digraph.replaceConnection(this.mockRegion1, this.regionFilter1, this.mockRegion2);
		assertEquals("Wrong existing filter.", null, existing);

		existing = this.digraph.replaceConnection(this.mockRegion1, this.regionFilter2, this.mockRegion2);
		assertEquals("Wrong existing filter.", this.regionFilter1, existing);

		existing = this.digraph.replaceConnection(this.mockRegion1, null, this.mockRegion2);
		assertEquals("Wrong existing filter.", this.regionFilter2, existing);

		existing = this.digraph.replaceConnection(this.mockRegion1, this.regionFilter1, this.mockRegion2);
		assertEquals("Wrong existing filter.", null, existing);
	}

	@Test
	public void testGetEdges() throws BundleException {
		setDefaultFilters();

		this.digraph.connect(this.mockRegion1, this.regionFilter1, this.mockRegion2);
		this.digraph.connect(this.mockRegion1, this.regionFilter2, this.mockRegion3);
		this.digraph.connect(this.mockRegion2, this.regionFilter2, this.mockRegion1);

		Set<FilteredRegion> edges = this.digraph.getEdges(this.mockRegion1);

		assertEquals(2, edges.size());

		for (FilteredRegion edge : edges) {
			if (edge.getRegion().equals(this.mockRegion2)) {
				assertEquals(this.regionFilter1, edge.getFilter());
			} else if (edge.getRegion().equals(this.mockRegion3)) {
				assertEquals(this.regionFilter2, edge.getFilter());
			} else {
				fail("unexpected edge");
			}
		}

		this.digraph.replaceConnection(this.mockRegion1, this.regionFilter2, this.mockRegion2);

		edges = this.digraph.getEdges(this.mockRegion1);
		assertEquals(2, edges.size());

		for (FilteredRegion edge : edges) {
			if (edge.getRegion().equals(this.mockRegion2)) {
				assertEquals(this.regionFilter2, edge.getFilter());
			} else if (edge.getRegion().equals(this.mockRegion3)) {
				assertEquals(this.regionFilter2, edge.getFilter());
			} else {
				fail("unexpected edge");
			}
		}
	}

	@Test
	public void testRemoveRegion() throws BundleException {
		setDefaultFilters();

		this.digraph.connect(this.mockRegion1, this.regionFilter1, this.mockRegion2);
		this.digraph.connect(this.mockRegion2, this.regionFilter2, this.mockRegion1);
		assertNotNull(this.digraph.getRegion("mockRegion1"));
		assertNotNull(this.digraph.getRegion("mockRegion2"));
		this.digraph.removeRegion(this.mockRegion1);
		assertNull(this.digraph.getRegion("mockRegion1"));
		assertNotNull(this.digraph.getRegion("mockRegion2"));
	}

	@Test
	public void testGetRegions() throws BundleException {
		setDefaultFilters();

		this.digraph.connect(this.mockRegion1, this.regionFilter1, this.mockRegion2);
		Set<Region> regions = this.digraph.getRegions();
		assertEquals(2, regions.size());
		assertTrue(regions.contains(this.mockRegion1));
		assertTrue(regions.contains(this.mockRegion2));
	}

	private static final String REGION_A = "A";
	private static final String REGION_B = "B";
	private static final String REGION_C = "C";
	private static final String REGION_D = "D";

	@Test
	public void testCopyRegion() throws BundleException, InvalidSyntaxException {
		RegionDigraph testDigraph = RegionReflectionUtils.newStandardRegionDigraph();
		long bundleId = 1;
		Region a = testDigraph.createRegion(REGION_A);
		a.addBundle(bundleId++);
		a.addBundle(bundleId++);
		Region b = testDigraph.createRegion(REGION_B);
		b.addBundle(bundleId++);
		b.addBundle(bundleId++);
		Region c = testDigraph.createRegion(REGION_C);
		c.addBundle(bundleId++);
		c.addBundle(bundleId++);
		Region d = testDigraph.createRegion(REGION_D);
		d.addBundle(bundleId++);
		d.addBundle(bundleId++);

		testDigraph.connect(a, testDigraph.createRegionFilterBuilder().allow("a", "(a=x)").build(), b);
		testDigraph.connect(b, testDigraph.createRegionFilterBuilder().allow("b", "(b=x)").build(), c);
		testDigraph.connect(c, testDigraph.createRegionFilterBuilder().allow("c", "(c=x)").build(), d);
		testDigraph.connect(d, testDigraph.createRegionFilterBuilder().allow("d", "(d=x)").build(), a);
		RegionDigraph testCopy = testDigraph.copy();
		StandardRegionDigraphPeristenceTests.assertEquals(testDigraph, testCopy);

		a = testCopy.getRegion(REGION_A);
		b = testCopy.getRegion(REGION_B);
		c = testCopy.getRegion(REGION_C);
		d = testCopy.getRegion(REGION_D);

		for (Region region : testCopy) {
			testCopy.removeRegion(region);
		}

		testCopy.connect(a, testCopy.createRegionFilterBuilder().allow("a", "(a=x)").build(), d);
		testCopy.connect(b, testCopy.createRegionFilterBuilder().allow("b", "(b=x)").build(), a);
		testCopy.connect(c, testCopy.createRegionFilterBuilder().allow("c", "(c=x)").build(), b);
		testCopy.connect(d, testCopy.createRegionFilterBuilder().allow("d", "(d=x)").build(), c);

		try {
			StandardRegionDigraphPeristenceTests.assertEquals(testDigraph, testCopy);
			fail("Digraphs must not be equal");
		} catch (AssertionError e) {
			// expected
		}
		testDigraph.replace(testCopy);
		StandardRegionDigraphPeristenceTests.assertEquals(testDigraph, testCopy);

		// test that we can continue to use the copy to replace as long as it is upto
		// date with the last replace
		Region testAdd1 = testCopy.createRegion("testAdd1");
		testCopy.connect(testAdd1, testCopy.createRegionFilterBuilder().allow("testAdd1", "(testAdd=x)").build(), a);
		try {
			StandardRegionDigraphPeristenceTests.assertEquals(testDigraph, testCopy);
			fail("Digraphs must not be equal");
		} catch (AssertionError e) {
			// expected
		}
		testDigraph.replace(testCopy);
		StandardRegionDigraphPeristenceTests.assertEquals(testDigraph, testCopy);

		// test that we fail if the digraph was modified since last copy/replace
		testCopy = testDigraph.copy();
		// add a new bundle to the original
		Region origA = testDigraph.getRegion(REGION_A);
		origA.addBundle(bundleId++);
		try {
			testDigraph.replace(testCopy);
			fail("Digraph changed since copy.");
		} catch (BundleException e) {
			// expected
		}

		// test that we fail if the digraph was modified since last copy/replace
		testCopy = testDigraph.copy();
		// add a new bundle to the original
		origA.removeBundle(bundleId);
		try {
			testDigraph.replace(testCopy);
			fail("Digraph changed since copy.");
		} catch (BundleException e) {
			// expected
		}

		testCopy = testDigraph.copy();
		// add a new region to the original
		Region testAdd2 = testDigraph.createRegion("testAdd2");
		testDigraph.connect(testAdd2, testCopy.createRegionFilterBuilder().allow("testAdd2", "(testAdd=x)").build(),
				origA);
		try {
			testDigraph.replace(testCopy);
			fail("Digraph changed since copy.");
		} catch (BundleException e) {
			// expected
		}

		testCopy = testDigraph.copy();
		// change a connection in the original
		testDigraph.removeRegion(testAdd2);
		testDigraph.connect(testAdd2, testCopy.createRegionFilterBuilder().allow("testAdd2", "(testAdd=y)").build(),
				origA);
		try {
			testDigraph.replace(testCopy);
			fail("Digraph changed since copy.");
		} catch (BundleException e) {
			// expected
		}

		testCopy = testDigraph.copy();
		Region origB = testDigraph.getRegion(REGION_B);
		// add a connection in the original
		testDigraph.connect(testAdd2, testCopy.createRegionFilterBuilder().allow("testAdd2", "(testAdd=y)").build(),
				origB);
		try {
			testDigraph.replace(testCopy);
			fail("Digraph changed since copy.");
		} catch (BundleException e) {
			// expected
		}
	}

	static class TestRegionDigraphVisitor implements RegionDigraphVisitor {
		final Collection<Region> visited = new ArrayList<>();
		final String namespace;
		final Map<String, ?> attributes;

		public TestRegionDigraphVisitor(String namespace, Map<String, ?> attributes) {
			super();
			this.namespace = namespace;
			this.attributes = attributes;
		}

		@Override
		public boolean visit(Region region) {
			visited.add(region);
			return true;
		}

		@Override
		public boolean preEdgeTraverse(RegionFilter regionFilter) {
			return regionFilter.isAllowed(namespace, attributes);
		}

		@Override
		public void postEdgeTraverse(RegionFilter regionFilter) {
			// nothing
		}

		Collection<Region> clearVisited() {
			Collection<Region> result = new ArrayList<>(visited);
			visited.clear();
			return result;
		}
	}

	@Test
	public void testVisitRegions() throws BundleException, InvalidSyntaxException {
		RegionDigraph testDigraph = RegionReflectionUtils.newStandardRegionDigraph();
		Region a = testDigraph.createRegion(REGION_A);
		Region b = testDigraph.createRegion(REGION_B);
		Region c = testDigraph.createRegion(REGION_C);
		Region d = testDigraph.createRegion(REGION_D);

		testDigraph.connect(a, testDigraph.createRegionFilterBuilder().allow("b", "(b=x)").allow("c", "(c=x)")
				.allow("d", "(d=x)").build(), b);
		testDigraph.connect(b, testDigraph.createRegionFilterBuilder().allow("c", "(c=x)").allow("d", "(d=x)").build(),
				c);
		testDigraph.connect(c, testDigraph.createRegionFilterBuilder().allow("d", "(d=x)").build(), d);

		Map<String, String> attributes = new HashMap<>();
		attributes.put("d", "x");
		TestRegionDigraphVisitor visitor = new TestRegionDigraphVisitor("d", attributes);

		Collection<Region> expected = new ArrayList<>(Arrays.asList(a, b, c, d));
		for (Region region : expected.toArray(new Region[0])) {
			testDigraph.visitSubgraph(region, visitor);
			Collection<Region> visited = visitor.clearVisited();
			assertEquals("Wrong number of visited: " + region, expected.size(), visited.size());
			assertTrue("Wrong visited content: " + region, visited.containsAll(expected));
			expected.remove(region);
		}

		attributes.clear();
		attributes.put("c", "x");
		visitor = new TestRegionDigraphVisitor("c", attributes);
		expected = new ArrayList<>(Arrays.asList(a, b, c));
		for (Region region : expected.toArray(new Region[0])) {
			testDigraph.visitSubgraph(region, visitor);
			Collection<Region> visited = visitor.clearVisited();
			assertEquals("Wrong number of visited: " + region, expected.size(), visited.size());
			assertTrue("Wrong visited content: " + region, visited.containsAll(expected));
			expected.remove(region);
		}

		attributes.clear();
		attributes.put("b", "x");
		visitor = new TestRegionDigraphVisitor("b", attributes);
		expected = new ArrayList<>(Arrays.asList(a, b));
		for (Region region : expected.toArray(new Region[0])) {
			testDigraph.visitSubgraph(region, visitor);
			Collection<Region> visited = visitor.clearVisited();
			assertEquals("Wrong number of visited: " + region, expected.size(), visited.size());
			assertTrue("Wrong visited content: " + region, visited.containsAll(expected));
			expected.remove(region);
		}
	}

	@Test
	public void testGetHooks() throws BundleException {
		setDefaultFilters();

		assertNotNull("Resolver Hook is null", digraph.getResolverHookFactory());
		assertNotNull("Bundle Event Hook is null", digraph.getBundleEventHook());
		assertNotNull("Bundle Find Hook is null", digraph.getBundleFindHook());
		assertNotNull("Servie Event Hook is null", digraph.getServiceEventHook());
		assertNotNull("Service Find Hook is null", digraph.getServiceFindHook());

		RegionDigraph copy = digraph.copy();
		assertNotNull("Resolver Hook is null", copy.getResolverHookFactory());
		assertNotNull("Bundle Event Hook is null", copy.getBundleEventHook());
		assertNotNull("Bundle Find Hook is null", copy.getBundleFindHook());
		assertNotNull("Servie Event Hook is null", copy.getServiceEventHook());
		assertNotNull("Service Find Hook is null", copy.getServiceFindHook());
	}

}
