/*******************************************************************************
 * Copyright (c) 2019 Sebastian Zarnekow and others.
 *
 * 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:
 *     Sebastian Zarnekow - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.core.tests.builder;

import static org.junit.Assert.assertArrayEquals;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collections;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.core.builder.ReferenceCollection;

import junit.framework.Test;

public class ReferenceCollectionTest extends BuilderTests {

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

	public static Test suite() {
		return buildTestSuite(ReferenceCollectionTest.class);
	}

	private static class TestableReferenceCollection extends ReferenceCollection {
		/*
		    Make the package visible fields reflectively available for testing
			char[][][] qualifiedNameReferences;
			char[][] simpleNameReferences;
			char[][] rootReferences;
		 */
		protected TestableReferenceCollection(char[][][] qualifiedNameReferences, char[][] simpleNameReferences,
				char[][] rootReferences) {
			super(qualifiedNameReferences, simpleNameReferences, rootReferences);
		}

		char[][][] getQualifiedNameReferences() {
			try {
				Field fld = ReferenceCollection.class.getDeclaredField("qualifiedNameReferences");
				fld.setAccessible(true);
				return (char[][][]) fld.get(this);
			} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
				throw new RuntimeException(e);
			}
		}

		char[][] getSimpleNameReferences() {
			try {
				Field fld = ReferenceCollection.class.getDeclaredField("simpleNameReferences");
				fld.setAccessible(true);
				return (char[][]) fld.get(this);
			} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
				throw new RuntimeException(e);
			}
		}

		char[][] getRootReferences() {
			try {
				Field fld = ReferenceCollection.class.getDeclaredField("rootReferences");
				fld.setAccessible(true);
				return (char[][]) fld.get(this);
			} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
				throw new RuntimeException(e);
			}
		}
	}

	public void testInternQualifiedNamesSorts_01() {
		char[][][] qualifiedNames = new char[][][] {
			CharOperation.splitOn('.', "java.lang.RuntimeException".toCharArray()),
			CharOperation.splitOn('.', "a.a.a".toCharArray()),
			CharOperation.splitOn('.', "b.b.b".toCharArray()),
			CharOperation.splitOn('.', "a.a".toCharArray()),
			CharOperation.splitOn('.', "a.b".toCharArray()),
			CharOperation.splitOn('.', "com".toCharArray()),
			CharOperation.splitOn('.', "a".toCharArray()),
			CharOperation.splitOn('.', "b".toCharArray())
		};
		char[][][] expectation = qualifiedNames.clone();
		Collections.shuffle(Arrays.asList(qualifiedNames));
		char[][][] internQualifiedNames = ReferenceCollection.internQualifiedNames(qualifiedNames, true);
		assertArrayEquals("Should be sorted, longest first, alphanumeric later",
				toStringArray(expectation),
				toStringArray(internQualifiedNames));
	}

	public void testInternQualifiedNamesSorts_02() {
		char[][][] qualifiedNames = new char[][][] {
			CharOperation.splitOn('.', "java.lang.RuntimeException".toCharArray()),
			CharOperation.splitOn('.', "a.a.a".toCharArray()),
			CharOperation.splitOn('.', "b.b.b".toCharArray()),
			CharOperation.splitOn('.', "a.a".toCharArray()),
			CharOperation.splitOn('.', "a.b".toCharArray()),
			CharOperation.splitOn('.', "com".toCharArray()),
			CharOperation.splitOn('.', "a".toCharArray()),
			CharOperation.splitOn('.', "b".toCharArray())
		};
		char[][][] expectation = new char[][][] {
			CharOperation.splitOn('.', "a.a.a".toCharArray()),
			CharOperation.splitOn('.', "b.b.b".toCharArray()),
			CharOperation.splitOn('.', "a.a".toCharArray()),
			CharOperation.splitOn('.', "a.b".toCharArray()),
			CharOperation.splitOn('.', "a".toCharArray()),
			CharOperation.splitOn('.', "b".toCharArray())
		};
		Collections.shuffle(Arrays.asList(qualifiedNames));
		char[][][] internQualifiedNames = ReferenceCollection.internQualifiedNames(qualifiedNames, false);
		assertArrayEquals("Should be sorted, longest first, alphanumeric later",
				toStringArray(expectation),
				toStringArray(internQualifiedNames));
	}

	public void testInternSimpleNamesSorts_01() {
		char[][] simpleNames = new char[][] {
			"Throwable".toCharArray(),
			"aaa".toCharArray(),
			"bbb".toCharArray(),
			"ccc".toCharArray(),
			"aa".toCharArray(),
			"a".toCharArray()
		};
		char[][] expectation = simpleNames.clone();
		Collections.shuffle(Arrays.asList(simpleNames));
		char[][] internSimpleNames = ReferenceCollection.internSimpleNames(simpleNames, false);
		assertArrayEquals("Should be sorted, longest first, alphanumeric later",
				toStringArray(expectation),
				toStringArray(internSimpleNames));
	}

	public void testInternSimpleNamesSorts_02() {
		char[][] simpleNames = new char[][] {
			"aaa".toCharArray(),
			"bbb".toCharArray(),
			"Throwable".toCharArray(),
			"ccc".toCharArray(),
			"java".toCharArray(),
			"aa".toCharArray(),
			"a".toCharArray()
		};
		char[][] expectation = new char[][] {
			"aaa".toCharArray(),
			"bbb".toCharArray(),
			"ccc".toCharArray(),
			"aa".toCharArray(),
			"a".toCharArray()
		};
		Collections.shuffle(Arrays.asList(simpleNames));
		char[][] internSimpleNames = ReferenceCollection.internSimpleNames(simpleNames, true);
		assertArrayEquals("Should be sorted, longest first, alphanumeric later",
				toStringArray(expectation),
				toStringArray(internSimpleNames));
	}

	public void testIncludesWithBinarySearch() {
		char[][] simpleNames = ReferenceCollection.internSimpleNames(new char[][] {
			"a".toCharArray(),
			"b".toCharArray(),
			"c".toCharArray(),
			"d".toCharArray(),
			"e".toCharArray(),
			"f".toCharArray(),
			"g".toCharArray(),
			"h".toCharArray(),
			"i".toCharArray(),
			"j".toCharArray(),
			"k".toCharArray(),
			"l".toCharArray(),
			"m".toCharArray(),
			"n".toCharArray(),
			"o".toCharArray(),
			"p".toCharArray(),
			"q".toCharArray(),
			"r".toCharArray(),
			"s".toCharArray(),
			"t".toCharArray(),
			"u".toCharArray(),
			"v".toCharArray(),
			"w".toCharArray(),
			"x".toCharArray(),
			"y".toCharArray(),
			"z".toCharArray()
		}, false);
		ReferenceCollection collection = new TestableReferenceCollection(null, simpleNames, null);
		for(char[] simpleName: simpleNames) {
			assertTrue("Should include " + simpleName[0], collection.includes(simpleName));
			assertFalse("Should not include " + CharOperation.toUpperCase(simpleName)[0], collection.includes(CharOperation.toUpperCase(simpleName)));
		}
	}

	public void testIncludesWithLinearSearch() {
		char[][] simpleNames = ReferenceCollection.internSimpleNames(new char[][] {
			"a".toCharArray(),
			"b".toCharArray(),
			"c".toCharArray(),
			"d".toCharArray(),
			"e".toCharArray(),
			"f".toCharArray(),
			"g".toCharArray(),
			"h".toCharArray()
		}, false);
		ReferenceCollection collection = new TestableReferenceCollection(null, simpleNames, null);
		for(char[] simpleName: simpleNames) {
			assertTrue("Should include " + simpleName[0], collection.includes(simpleName));
			assertFalse("Should not include " + CharOperation.toUpperCase(simpleName)[0], collection.includes(CharOperation.toUpperCase(simpleName)));
		}
	}

	public void testIncludes01() {
		TestableReferenceCollection refColl = new TestableReferenceCollection(null, null, null);

		String[] array = new String[] { "a.a", "b.a", "b.b" };
		refColl.addDependencies(array);

		TestableReferenceCollection other = new TestableReferenceCollection(null, null, null);
		String[] array2 = new String[] { "a.a", "B.A", "b.b" };
		other.addDependencies(array2);

		assertTrue(refColl.includes(other.getQualifiedNameReferences(), other.getSimpleNameReferences(), other.getRootReferences()));
		assertTrue(other.includes(refColl.getQualifiedNameReferences(), refColl.getSimpleNameReferences(), refColl.getRootReferences()));
	}

	public void testIncludes02() {
		TestableReferenceCollection refColl = new TestableReferenceCollection(null, null, null);

		String[] array = new String[] { "a.x", "b.y" };
		refColl.addDependencies(array);

		TestableReferenceCollection other = new TestableReferenceCollection(null, null, null);
		String[] array2 = new String[] { "a.y", "b.x" };
		other.addDependencies(array2);

		assertTrue(refColl.includes(other.getQualifiedNameReferences(), other.getSimpleNameReferences(), other.getRootReferences()));
		assertTrue(other.includes(refColl.getQualifiedNameReferences(), refColl.getSimpleNameReferences(), refColl.getRootReferences()));
	}

	public void testIncludes03() {
		TestableReferenceCollection refColl = new TestableReferenceCollection(null, null, null);

		String[] array = new String[] { "a.d.y", "c.x" };
		refColl.addDependencies(array);

		TestableReferenceCollection other = new TestableReferenceCollection(null, null, null);
		String[] array2 = new String[] { "c.y" };
		other.addDependencies(array2);

		assertTrue(refColl.includes(other.getQualifiedNameReferences(), other.getSimpleNameReferences(), other.getRootReferences()));
		assertTrue(other.includes(refColl.getQualifiedNameReferences(), refColl.getSimpleNameReferences(), refColl.getRootReferences()));
	}

	public void testIncludes04() {
		TestableReferenceCollection refColl = new TestableReferenceCollection(null, null, null);

		String[] array = new String[] { "a.d.y" };
		refColl.addDependencies(array);

		TestableReferenceCollection other = new TestableReferenceCollection(null, null, null);
		String[] array2 = new String[] { "a.d" };
		other.addDependencies(array2);

		assertTrue(refColl.includes(other.getQualifiedNameReferences(), other.getSimpleNameReferences(), other.getRootReferences()));
		assertTrue(other.includes(refColl.getQualifiedNameReferences(), refColl.getSimpleNameReferences(), refColl.getRootReferences()));
	}

	public void testIncludesNot() {
		TestableReferenceCollection refColl = new TestableReferenceCollection(null, null, null);

		String[] array = new String[] {"b.a"};
		refColl.addDependencies(array);

		TestableReferenceCollection other = new TestableReferenceCollection(null, null, null);
		String[] array2 = new String[] {"B.A"};
		other.addDependencies(array2);

		assertFalse(refColl.includes(other.getQualifiedNameReferences(), other.getSimpleNameReferences(), other.getRootReferences()));
		assertFalse(other.includes(refColl.getQualifiedNameReferences(), refColl.getSimpleNameReferences(), refColl.getRootReferences()));
	}

	public void testAddDependencies() {
		TestableReferenceCollection refColl = new TestableReferenceCollection(null, null, null);

		String[] array = new String[] {"a.b.c.D"};
		refColl.addDependencies(array);

		char[][][] qualifiedNameReferences = refColl.getQualifiedNameReferences();
		String [] strings = toStringArray(qualifiedNameReferences);
		assertArrayEquals(new String[] {
			"a.b.c.D",
			"a.b.c",
			"a.b",
			"a"
		}, strings);

		char[][] simpleNameReferences = refColl.getSimpleNameReferences();
		assertArrayEquals(new String[] {
			"D",
			"a",
			"b",
			"c"
		}, CharOperation.toStrings(simpleNameReferences));

		char[][] rootReferences = refColl.getRootReferences();
		assertArrayEquals(new String[] {
			"a"
		}, CharOperation.toStrings(rootReferences));
	}

	private static String[] toStringArray(char[][][] qualifiedNameReferences) {
		return Arrays.stream(qualifiedNameReferences).map(a -> CharOperation.toString(a)).toArray(String[]::new);
	}

	private static String[] toStringArray(char[][] qualifiedNameReferences) {
		return Arrays.stream(qualifiedNameReferences).map(a -> CharOperation.charToString(a)).toArray(String[]::new);
	}

	public void testRegression01() {
		char[][][] qualifiedNames = new char[][][] {
			CharOperation.splitOn('.', "p1.IX".toCharArray()),
			CharOperation.splitOn('.', "p2.X".toCharArray())
		};
		char[][] simpleNames = new char[][] {
			"I__X".toCharArray(), "IX".toCharArray(), "p1".toCharArray(), "p2".toCharArray(), "X".toCharArray()
		};
		char[][] rootNames = new char[][] {
			"java".toCharArray(), "IX".toCharArray(), "p1".toCharArray(), "p2".toCharArray()
		};
		ReferenceCollection collection = new TestableReferenceCollection(qualifiedNames, simpleNames, rootNames);
		qualifiedNames = ReferenceCollection.internQualifiedNames(new char[][][] {
			CharOperation.splitOn('.', "p2".toCharArray())
		});
		simpleNames = ReferenceCollection.internSimpleNames(new char[][] {
			"X".toCharArray()
		}, true);
		rootNames = ReferenceCollection.internSimpleNames(new char[][] {
			"p2".toCharArray()
		}, false);

		assertTrue("Should include", collection.includes(qualifiedNames, simpleNames, rootNames));
	}

	public void testRegression02() {
		char[][][] qualifiedNames = new char[][][] {};
		char[][] simpleNames = new char[][] {
			"GeneratedAnnotation".toCharArray(), "Test".toCharArray()
		};
		char[][] rootNames = new char[][] {
			"java".toCharArray(), "GeneratedAnnotation".toCharArray()
		};
		ReferenceCollection collection = new TestableReferenceCollection(qualifiedNames, simpleNames, rootNames);

		qualifiedNames = null;
		simpleNames = ReferenceCollection.internSimpleNames(new char[][] {
			"GeneratedAnnotation".toCharArray()
		}, true);
		rootNames = ReferenceCollection.internSimpleNames(new char[][] {
			"GeneratedAnnotation".toCharArray()
		}, false);

		assertTrue("Should include", collection.includes(qualifiedNames, simpleNames, rootNames));
	}
}
