/*
 * Copyright (C) 2010, Red Hat Inc. and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0 which is available at
 * https://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */
package org.eclipse.jgit.attributes;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

/**
 * Tests git attributes pattern matches
 */
public class AttributesMatcherTest {

	@Test
	public void testBasic() {
		String pattern = "/test.stp";
		assertMatched(pattern, "/test.stp");

		pattern = "#/test.stp";
		assertNotMatched(pattern, "/test.stp");
	}

	@Test
	public void testFileNameWildcards() {
		//Test basic * and ? for any pattern + any character
		String pattern = "*.st?";
		assertMatched(pattern, "/test.stp");
		assertMatched(pattern, "/anothertest.stg");
		assertMatched(pattern, "/anothertest.st0");
		assertNotMatched(pattern, "/anothertest.sta1");
		//Check that asterisk does not expand to "/"
		assertNotMatched(pattern, "/another/test.sta1");

		//Same as above, with a leading slash to ensure that doesn't cause problems
		pattern = "/*.st?";
		assertMatched(pattern, "/test.stp");
		assertMatched(pattern, "/anothertest.stg");
		assertMatched(pattern, "/anothertest.st0");
		assertNotMatched(pattern, "/anothertest.sta1");
		//Check that asterisk does not expand to "/"
		assertNotMatched(pattern, "/another/test.sta1");

		//Test for numbers
		pattern = "*.sta[0-5]";
		assertMatched(pattern,  "/test.sta5");
		assertMatched(pattern, "/test.sta4");
		assertMatched(pattern, "/test.sta3");
		assertMatched(pattern, "/test.sta2");
		assertMatched(pattern, "/test.sta1");
		assertMatched(pattern, "/test.sta0");
		assertMatched(pattern, "/anothertest.sta2");
		assertNotMatched(pattern, "test.stag");
		assertNotMatched(pattern, "test.sta6");

		//Test for letters
		pattern = "/[tv]est.sta[a-d]";
		assertMatched(pattern,  "/test.staa");
		assertMatched(pattern, "/test.stab");
		assertMatched(pattern, "/test.stac");
		assertMatched(pattern, "/test.stad");
		assertMatched(pattern, "/vest.stac");
		assertNotMatched(pattern, "test.stae");
		assertNotMatched(pattern, "test.sta9");

		//Test child directory/file is matched
		pattern = "/src/ne?";
		assertMatched(pattern, "/src/new/");
		assertMatched(pattern, "/src/new");
		assertNotMatched(pattern, "/src/new/a.c");
		assertNotMatched(pattern, "/src/new/a/a.c");
		assertNotMatched(pattern, "/src/new.c");

		//Test name-only fnmatcher matches
		pattern = "ne?";
		assertMatched(pattern, "/src/new/");
		assertMatched(pattern, "/src/new");
		assertNotMatched(pattern, "/src/new/a.c");
		assertNotMatched(pattern, "/src/new/a/a.c");
		assertMatched(pattern, "/neb");
		assertNotMatched(pattern, "/src/new.c");
	}

	@Test
	public void testTargetWithoutLeadingSlash() {
		//Test basic * and ? for any pattern + any character
		String pattern = "/*.st?";
		assertMatched(pattern, "test.stp");
		assertMatched(pattern, "anothertest.stg");
		assertMatched(pattern, "anothertest.st0");
		assertNotMatched(pattern, "anothertest.sta1");
		//Check that asterisk does not expand to ""
		assertNotMatched(pattern, "another/test.sta1");

		//Same as above, with a leading slash to ensure that doesn't cause problems
		pattern = "/*.st?";
		assertMatched(pattern, "test.stp");
		assertMatched(pattern, "anothertest.stg");
		assertMatched(pattern, "anothertest.st0");
		assertNotMatched(pattern, "anothertest.sta1");
		//Check that asterisk does not expand to ""
		assertNotMatched(pattern, "another/test.sta1");

		//Test for numbers
		pattern = "/*.sta[0-5]";
		assertMatched(pattern,  "test.sta5");
		assertMatched(pattern, "test.sta4");
		assertMatched(pattern, "test.sta3");
		assertMatched(pattern, "test.sta2");
		assertMatched(pattern, "test.sta1");
		assertMatched(pattern, "test.sta0");
		assertMatched(pattern, "anothertest.sta2");
		assertNotMatched(pattern, "test.stag");
		assertNotMatched(pattern, "test.sta6");

		//Test for letters
		pattern = "/[tv]est.sta[a-d]";
		assertMatched(pattern,  "test.staa");
		assertMatched(pattern, "test.stab");
		assertMatched(pattern, "test.stac");
		assertMatched(pattern, "test.stad");
		assertMatched(pattern, "vest.stac");
		assertNotMatched(pattern, "test.stae");
		assertNotMatched(pattern, "test.sta9");

		//Test child directory/file is matched
		pattern = "/src/ne?";
		assertMatched(pattern, "src/new/");
		assertMatched(pattern, "src/new");
		assertNotMatched(pattern, "src/new/a.c");
		assertNotMatched(pattern, "src/new/a/a.c");
		assertNotMatched(pattern, "src/new.c");

		//Test name-only fnmatcher matches
		pattern = "ne?";
		assertMatched(pattern, "src/new/");
		assertMatched(pattern, "src/new");
		assertNotMatched(pattern, "src/new/a.c");
		assertNotMatched(pattern, "src/new/a/a.c");
		assertMatched(pattern, "neb");
		assertNotMatched(pattern, "src/new.c");
	}

	@Test
	public void testParentDirectoryGitAttributes() {
		//Contains git attribute patterns such as might be seen in a parent directory

		//Test for wildcards
		String pattern = "/*/*.c";
		assertMatched(pattern, "/file/a.c");
		assertMatched(pattern, "/src/a.c");
		assertNotMatched(pattern, "/src/new/a.c");

		//Test child directory/file is matched
		pattern = "/src/new";
		assertMatched(pattern, "/src/new/");
		assertMatched(pattern, "/src/new");
		assertNotMatched(pattern, "/src/new/a.c");
		assertNotMatched(pattern, "/src/new/a/a.c");
		assertNotMatched(pattern, "/src/new.c");

		//Test child directory is matched, slash after name
		pattern = "/src/new/";
		assertMatched(pattern, "/src/new/");
		assertNotMatched(pattern, "/src/new/a.c");
		assertNotMatched(pattern, "/src/new/a/a.c");
		assertNotMatched(pattern, "/src/new");
		assertNotMatched(pattern, "/src/new.c");

		//Test directory is matched by name only
		pattern = "b1";
		assertNotMatched(pattern, "/src/new/a/b1/a.c");
		assertNotMatched(pattern, "/src/new/a/b2/file.c");
		assertNotMatched(pattern, "/src/new/a/bb1/file.c");
		assertNotMatched(pattern, "/src/new/a/file.c");
		assertNotMatched(pattern, "/src/new/a/bb1");
		assertMatched(pattern, "/src/new/a/b1");
	}

	@Test
	public void testTrailingSlash() {
		String pattern = "/src/";
		assertMatched(pattern, "/src/");
		assertNotMatched(pattern, "/src/new");
		assertNotMatched(pattern, "/src/new/a.c");
		assertNotMatched(pattern, "/src/a.c");
		assertNotMatched(pattern, "/src");
		assertNotMatched(pattern, "/srcA/");

		pattern = "src/";
		assertMatched(pattern, "src/");
		assertMatched(pattern, "/src/");
		assertNotMatched(pattern, "src");
		assertNotMatched(pattern, "/src/new");
		assertNotMatched(pattern, "/src/new/a.c");
		assertNotMatched(pattern, "/src/a.c");
		assertNotMatched(pattern, "foo/src/a.c");
		assertNotMatched(pattern, "foo/src/bar/a.c");
		assertNotMatched(pattern, "foo/src/bar/src");
		assertMatched(pattern, "foo/src/");
		assertMatched(pattern, "foo/src/bar/src/");
	}

	@Test
	public void testNameOnlyMatches() {
		/*
		 * Name-only matches do not contain any path separators
		 */
		//Test matches for file extension
		String pattern = "*.stp";
		assertMatched(pattern, "/test.stp");
		assertMatched(pattern, "/src/test.stp");
		assertNotMatched(pattern, "/test.stp1");
		assertNotMatched(pattern, "/test.astp");
		assertNotMatched(pattern, "test.stp/foo.bar");
		assertMatched(pattern, "test.stp");
		assertMatched(pattern, "test.stp/");
		assertMatched(pattern, "test.stp/test.stp");

		//Test matches for name-only, applies to file name or folder name
		pattern = "src";
		assertMatched(pattern, "/src");
		assertMatched(pattern, "/src/");
		assertNotMatched(pattern, "/src/a.c");
		assertNotMatched(pattern, "/src/new/a.c");
		assertNotMatched(pattern, "/new/src/a.c");
		assertMatched(pattern, "/file/src");

		//Test matches for name-only, applies only to folder names
		pattern = "src/";
		assertNotMatched(pattern, "/src/a.c");
		assertNotMatched(pattern, "/src/new/a.c");
		assertNotMatched(pattern, "/new/src/a.c");
		assertNotMatched(pattern, "/src");
		assertNotMatched(pattern, "/file/src");
		assertMatched(pattern, "/file/src/");

		//Test matches for name-only, applies to file name or folder name
		//With a small wildcard
		pattern = "?rc";
		assertNotMatched(pattern, "/src/a.c");
		assertNotMatched(pattern, "/src/new/a.c");
		assertNotMatched(pattern, "/new/src/a.c");
		assertMatched(pattern, "/new/src/");
		assertMatched(pattern, "/file/src");
		assertMatched(pattern, "/src/");

		//Test matches for name-only, applies to file name or folder name
		//With a small wildcard
		pattern = "?r[a-c]";
		assertNotMatched(pattern, "/src/a.c");
		assertNotMatched(pattern, "/src/new/a.c");
		assertNotMatched(pattern, "/new/src/a.c");
		assertMatched(pattern, "/file/src");
		assertMatched(pattern, "/src/");
		assertNotMatched(pattern, "/srb/a.c");
		assertNotMatched(pattern, "/grb/new/a.c");
		assertNotMatched(pattern, "/new/crb/a.c");
		assertMatched(pattern, "/file/3rb");
		assertMatched(pattern, "/xrb/");
		assertNotMatched(pattern, "/3ra/a.c");
		assertNotMatched(pattern, "/5ra/new/a.c");
		assertNotMatched(pattern, "/new/1ra/a.c");
		assertNotMatched(pattern, "/new/1ra/a.c/");
		assertMatched(pattern, "/file/dra");
		assertMatched(pattern, "/file/dra/");
		assertMatched(pattern, "/era/");
		assertNotMatched(pattern, "/crg");
		assertNotMatched(pattern, "/cr3");
	}

	@Test
	public void testGetters() {
		AttributesRule r = new AttributesRule("/pattern/", "");
		assertFalse(r.isNameOnly());
		assertTrue(r.isDirOnly());
		assertNotNull(r.getAttributes());
		assertTrue(r.getAttributes().isEmpty());
		assertEquals(r.getPattern(), "/pattern");

		r = new AttributesRule("/patter?/", "");
		assertFalse(r.isNameOnly());
		assertTrue(r.isDirOnly());
		assertNotNull(r.getAttributes());
		assertTrue(r.getAttributes().isEmpty());
		assertEquals(r.getPattern(), "/patter?");

		r = new AttributesRule("patt*", "");
		assertTrue(r.isNameOnly());
		assertFalse(r.isDirOnly());
		assertNotNull(r.getAttributes());
		assertTrue(r.getAttributes().isEmpty());
		assertEquals(r.getPattern(), "patt*");

		r = new AttributesRule("pattern", "attribute1");
		assertTrue(r.isNameOnly());
		assertFalse(r.isDirOnly());
		assertNotNull(r.getAttributes());
		assertFalse(r.getAttributes().isEmpty());
		assertEquals(r.getAttributes().size(), 1);
		assertEquals(r.getPattern(), "pattern");

		r = new AttributesRule("pattern", "attribute1 -attribute2");
		assertTrue(r.isNameOnly());
		assertFalse(r.isDirOnly());
		assertNotNull(r.getAttributes());
		assertEquals(r.getAttributes().size(), 2);
		assertEquals(r.getPattern(), "pattern");

		r = new AttributesRule("pattern", "attribute1 \t-attribute2 \t");
		assertTrue(r.isNameOnly());
		assertFalse(r.isDirOnly());
		assertNotNull(r.getAttributes());
		assertEquals(r.getAttributes().size(), 2);
		assertEquals(r.getPattern(), "pattern");

		r = new AttributesRule("pattern", "attribute1\t-attribute2\t");
		assertTrue(r.isNameOnly());
		assertFalse(r.isDirOnly());
		assertNotNull(r.getAttributes());
		assertEquals(r.getAttributes().size(), 2);
		assertEquals(r.getPattern(), "pattern");

		r = new AttributesRule("pattern", "attribute1\t -attribute2\t ");
		assertTrue(r.isNameOnly());
		assertFalse(r.isDirOnly());
		assertNotNull(r.getAttributes());
		assertEquals(r.getAttributes().size(), 2);
		assertEquals(r.getPattern(), "pattern");

		r = new AttributesRule("pattern",
				"attribute1 -attribute2  attribute3=value ");
		assertTrue(r.isNameOnly());
		assertFalse(r.isDirOnly());
		assertNotNull(r.getAttributes());
		assertEquals(r.getAttributes().size(), 3);
		assertEquals(r.getPattern(), "pattern");
		assertEquals(r.getAttributes().get(0).toString(), "attribute1");
		assertEquals(r.getAttributes().get(1).toString(), "-attribute2");
		assertEquals(r.getAttributes().get(2).toString(), "attribute3=value");
	}

	@Test
	public void testBracketsInGroup() {
		//combinations of brackets in brackets, escaped and not

		String[] patterns = new String[]{"[[\\]]", "[\\[\\]]"};
		for (String pattern : patterns) {
			assertNotMatched(pattern, "");
			assertNotMatched(pattern, "[]");
			assertNotMatched(pattern, "][");
			assertNotMatched(pattern, "[\\[]");
			assertNotMatched(pattern, "[[]");
			assertNotMatched(pattern, "[[]]");
			assertNotMatched(pattern, "[\\[\\]]");

			assertMatched(pattern, "[");
			assertMatched(pattern, "]");
		}

		patterns = new String[]{"[[]]", "[\\[]]"};
		for (String pattern : patterns) {
			assertNotMatched(pattern, "");
			assertMatched(pattern, "[]");
			assertNotMatched(pattern, "][");
			assertNotMatched(pattern, "[\\[]");
			assertNotMatched(pattern, "[[]");
			assertNotMatched(pattern, "[[]]");
			assertNotMatched(pattern, "[\\[\\]]");

			assertNotMatched(pattern, "[");
			assertNotMatched(pattern, "]");
		}
	}

	@Test
	public void testFileNameWithLineTerminator() {
		assertMatched("a?", "a\r");
		assertMatched("a?", "dir/a\r");
		assertMatched("*a", "\ra");
		assertMatched("dir/*a*", "dir/\ra\r");
	}

	/**
	 * Check for a match. If target ends with "/", match will assume that the
	 * target is meant to be a directory.
	 *
	 * @param pattern
	 *            Pattern as it would appear in a .gitattributes file
	 * @param target
	 *            Target file path relative to repository's GIT_DIR
	 */
	private void assertMatched(String pattern, String target) {
		boolean value = match(pattern, target);
		assertTrue("Expected a match for: " + pattern + " with: " + target,
				value);
	}

	/**
	 * Check for a match. If target ends with "/", match will assume that the
	 * target is meant to be a directory.
	 *
	 * @param pattern
	 *            Pattern as it would appear in a .gitattributes file
	 * @param target
	 *            Target file path relative to repository's GIT_DIR
	 */
	private void assertNotMatched(String pattern, String target) {
		boolean value = match(pattern, target);
		assertFalse("Expected no match for: " + pattern + " with: " + target,
				value);
	}

	/**
	 * Check for a match. If target ends with "/", match will assume that the
	 * target is meant to be a directory.
	 *
	 * @param pattern
	 *            Pattern as it would appear in a .gitattributes file
	 * @param target
	 *            Target file path relative to repository's GIT_DIR
	 * @return Result of {@link AttributesRule#isMatch(String, boolean)}
	 */
	private static boolean match(String pattern, String target) {
		AttributesRule r = new AttributesRule(pattern, "");
		//If speed of this test is ever an issue, we can use a presetRule field
		//to avoid recompiling a pattern each time.
		return r.isMatch(target, target.endsWith("/"));
	}
}
