// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
// * with the License.  You may obtain a copy of the License at                                                              *
// *                                                                                                                         *
// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
// *                                                                                                                         *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
// * specific language governing permissions and limitations under the License.                                              *
// ***************************************************************************************************************************
package org.apache.juneau.config;

import static org.apache.juneau.testutils.TestUtils.*;
import static org.junit.Assert.*;

import java.util.*;
import java.util.concurrent.*;

import org.apache.juneau.*;
import org.apache.juneau.config.event.*;
import org.apache.juneau.config.internal.*;
import org.apache.juneau.config.store.*;
import org.junit.*;

public class ConfigMapListenerTest {

	//-----------------------------------------------------------------------------------------------------------------
	// Sanity tests.
	//-----------------------------------------------------------------------------------------------------------------

	@Test
	public void testBasicDefaultSection() throws Exception {
		ConfigStore s = initStore("Foo.cfg",
			"foo=bar"
		);

		final CountDownLatch latch = new CountDownLatch(1);

		LatchedListener l = new LatchedListener(latch) {
			@Override
			public void check(ConfigEvents events) throws Exception {
				assertObjectEquals("['SET(foo = baz)']", events);
			}
		};

		ConfigMap cm = s.getMap("Foo.cfg");
		cm.register(l);
		cm.setEntry("", "foo", "baz", null, null, null);
		cm.commit();
		wait(latch);
		assertNull(l.error);
		cm.unregister(l);

		assertTextEquals("foo = baz|", cm.toString());
	}

	@Test
	public void testBasicNormalSection() throws Exception {
		ConfigStore s = initStore("Foo.cfg",
			"[S1]",
			"foo=bar"
		);

		final CountDownLatch latch = new CountDownLatch(1);

		LatchedListener l = new LatchedListener(latch) {
			@Override
			public void check(ConfigEvents events) throws Exception {
				assertObjectEquals("['SET(S1/foo = baz)']", events);
			}
		};

		ConfigMap cm = s.getMap("Foo.cfg");
		cm.register(l);
		cm.setEntry("S1", "foo", "baz", null, null, null);
		cm.commit();
		wait(latch);
		assertNull(l.error);
		cm.unregister(l);

		assertTextEquals("[S1]|foo = baz|", cm.toString());
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Add new entries.
	//-----------------------------------------------------------------------------------------------------------------

	@Test
	public void testAddNewEntries() throws Exception {
		ConfigStore s = initStore("Foo.cfg"
		);

		final CountDownLatch latch = new CountDownLatch(2);

		LatchedListener l = new LatchedListener(latch) {
			@Override
			public void check(ConfigEvents events) throws Exception {
				assertObjectEquals("['SET(k = vb)','SET(S1/k1 = v1b)']", events);
			}
		};

		ConfigMap cm = s.getMap("Foo.cfg");
		cm.register(l);
		cm.setEntry("", "k", "vb", null, null, null);
		cm.setEntry("S1", "k1", "v1b", null, null, null);
		cm.commit();
		wait(latch);
		assertNull(l.error);
		cm.unregister(l);

		assertTextEquals("k = vb|[S1]|k1 = v1b|", cm.toString());
	}

	@Test
	public void testAddNewEntriesWithAttributes() throws Exception {
		ConfigStore s = initStore("Foo.cfg"
		);

		final CountDownLatch latch = new CountDownLatch(2);

		LatchedListener l = new LatchedListener(latch) {
			@Override
			public void check(ConfigEvents events) throws Exception {
				assertObjectEquals("['SET(k^* = kb # C)','SET(S1/k1^* = k1b # C1)']", events);
			}
		};

		ConfigMap cm = s.getMap("Foo.cfg");
		cm.register(l);
		cm.setEntry("", "k", "kb", "^*", "C", Arrays.asList("#k"));
		cm.setEntry("S1", "k1", "k1b", "^*", "C1", Arrays.asList("#k1"));
		cm.commit();
		wait(latch);
		assertNull(l.error);
		cm.unregister(l);

		assertTextEquals("#k|k^* = kb # C|[S1]|#k1|k1^* = k1b # C1|", cm.toString());
	}

	@Test
	public void testAddExistingEntriesWithAttributes() throws Exception {
		ConfigStore s = initStore("Foo.cfg",
			"#ka",
			"k=va # Ca",
			"#S1",
			"[S1]",
			"#k1a",
			"k1=v1a # Cb"
		);

		final CountDownLatch latch = new CountDownLatch(2);

		LatchedListener l = new LatchedListener(latch) {
			@Override
			public void check(ConfigEvents events) throws Exception {
				assertObjectEquals("['SET(k^* = kb # Cb)','SET(S1/k1^* = k1b # Cb1)']", events);
			}
		};

		ConfigMap cm = s.getMap("Foo.cfg");
		cm.register(l);
		cm.setEntry("", "k", "kb", "^*", "Cb", Arrays.asList("#kb"));
		cm.setEntry("S1", "k1", "k1b", "^*", "Cb1", Arrays.asList("#k1b"));
		cm.commit();
		wait(latch);
		assertNull(l.error);
		cm.unregister(l);

		assertTextEquals("#kb|k^* = kb # Cb|#S1|[S1]|#k1b|k1^* = k1b # Cb1|", cm.toString());
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Remove existing entries.
	//-----------------------------------------------------------------------------------------------------------------

	@Test
	public void testRemoveExistingEntries() throws Exception {
		ConfigStore s = initStore("Foo.cfg",
			"k=v",
			"[S1]",
			"k1=v1"
		);

		final CountDownLatch latch = new CountDownLatch(2);

		LatchedListener l = new LatchedListener(latch) {
			@Override
			public void check(ConfigEvents events) throws Exception {
				assertObjectEquals("['REMOVE_ENTRY(k)','REMOVE_ENTRY(S1/k1)']", events);
			}
		};

		ConfigMap cm = s.getMap("Foo.cfg");
		cm.register(l);
		cm.removeEntry("", "k");
		cm.removeEntry("S1", "k1");
		cm.commit();
		wait(latch);
		assertNull(l.error);
		cm.unregister(l);

		assertTextEquals("[S1]|", cm.toString());
	}

	@Test
	public void testRemoveExistingEntriesWithAttributes() throws Exception {
		ConfigStore s = initStore("Foo.cfg",
			"#ka",
			"k=va # Ca",
			"#S1",
			"[S1]",
			"#k1a",
			"k1=v1a # Cb"
		);

		final CountDownLatch latch = new CountDownLatch(2);

		LatchedListener l = new LatchedListener(latch) {
			@Override
			public void check(ConfigEvents events) throws Exception {
				assertObjectEquals("['REMOVE_ENTRY(k)','REMOVE_ENTRY(S1/k1)']", events);
			}
		};

		ConfigMap cm = s.getMap("Foo.cfg");
		cm.register(l);
		cm.removeEntry("", "k");
		cm.removeEntry("S1", "k1");
		cm.commit();
		wait(latch);
		assertNull(l.error);
		cm.unregister(l);

		assertTextEquals("#S1|[S1]|", cm.toString());
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Add new sections.
	//-----------------------------------------------------------------------------------------------------------------

	@Test
	public void testAddNewSections() throws Exception {
		ConfigStore s = initStore("Foo.cfg"
		);

		final CountDownLatch latch = new CountDownLatch(1);

		LatchedListener l = new LatchedListener(latch) {
			@Override
			public void check(ConfigEvents events) throws Exception {
				assertObjectEquals("['SET(S3/k3 = v3)']", events);
			}
		};

		ConfigMap cm = s.getMap("Foo.cfg");
		cm.register(l);
		cm.setSection("", Arrays.asList("#D1"));
		cm.setSection("S1", Arrays.asList("#S1"));
		cm.setSection("S2", null);
		cm.setSection("S3", Collections.<String>emptyList());
		cm.setEntry("S3", "k3", "v3", null, null, null);
		cm.commit();
		wait(latch);
		assertNull(l.error);
		cm.unregister(l);

		assertTextEquals("#D1||#S1|[S1]|[S2]|[S3]|k3 = v3|", cm.toString());
	}

	@Test
	public void testModifyExistingSections() throws Exception {
		ConfigStore s = initStore("Foo.cfg",
			"#Da",
			"",
			"#S1a",
			"[S1]",
			"[S2]",
			"[S3]"
		);

		final CountDownLatch latch = new CountDownLatch(1);

		LatchedListener l = new LatchedListener(latch) {
			@Override
			public void check(ConfigEvents events) throws Exception {
				assertObjectEquals("['SET(S3/k3 = v3)']", events);
			}
		};

		ConfigMap cm = s.getMap("Foo.cfg");
		cm.register(l);
		cm.setSection("", Arrays.asList("#Db"));
		cm.setSection("S1", Arrays.asList("#S1b"));
		cm.setSection("S2", null);
		cm.setSection("S3", Collections.<String>emptyList());
		cm.setEntry("S3", "k3", "v3", null, null, null);
		cm.commit();
		wait(latch);
		assertNull(l.error);
		cm.unregister(l);

		assertTextEquals("#Db||#S1b|[S1]|[S2]|[S3]|k3 = v3|", cm.toString());
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Remove sections.
	//-----------------------------------------------------------------------------------------------------------------

	@Test
	public void testRemoveSections() throws Exception {
		ConfigStore s = initStore("Foo.cfg",
			"#Da",
			"",
			"k = v",
			"",
			"#S1",
			"[S1]",
			"#k1",
			"k1 = v1",
			"[S2]",
			"#k2",
			"k2 = v2",
			"[S3]"
		);

		final CountDownLatch latch = new CountDownLatch(3);

		LatchedListener l = new LatchedListener(latch) {
			@Override
			public void check(ConfigEvents events) throws Exception {
				assertObjectEquals("['REMOVE_ENTRY(k)','REMOVE_ENTRY(S1/k1)','REMOVE_ENTRY(S2/k2)']", events);
			}
		};

		ConfigMap cm = s.getMap("Foo.cfg");
		cm.register(l);
		cm.removeSection("");
		cm.removeSection("S1");
		cm.removeSection("S2");
		cm.removeSection("S3");
		cm.commit();
		wait(latch);
		assertNull(l.error);
		cm.unregister(l);

		assertTextEquals("", cm.toString());
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Update from store.
	//-----------------------------------------------------------------------------------------------------------------

	@Test
	public void testUpdateFromStore() throws Exception {
		ConfigStore s = initStore("Foo.cfg");

		final CountDownLatch latch = new CountDownLatch(3);

		LatchedListener l = new LatchedListener(latch) {
			@Override
			public void check(ConfigEvents events) throws Exception {
				assertObjectEquals("['SET(k = v # cv)','SET(S1/k1 = v1 # cv1)','SET(S2/k2 = v2 # cv2)']", events);
			}
		};

		ConfigMap cm = s.getMap("Foo.cfg");
		cm.register(l);
		s.update("Foo.cfg",
			"#Da",
			"",
			"k = v # cv",
			"",
			"#S1",
			"[S1]",
			"#k1",
			"k1 = v1 # cv1",
			"[S2]",
			"#k2",
			"k2 = v2 # cv2",
			"[S3]"
		);
		wait(latch);
		assertNull(l.error);
		cm.unregister(l);

		assertTextEquals("#Da||k = v # cv||#S1|[S1]|#k1|k1 = v1 # cv1|[S2]|#k2|k2 = v2 # cv2|[S3]|", cm.toString());
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Merges.
	//-----------------------------------------------------------------------------------------------------------------

	@Test
	public void testMergeNoOverwrite() throws Exception {
		ConfigStore s = initStore("Foo.cfg",
			"[S1]",
			"k1 = v1a"
		);

		final CountDownLatch latch = new CountDownLatch(2);
		final Queue<String> eventList = new ConcurrentLinkedQueue<>();
		eventList.add("['SET(S1/k1 = v1b)']");
		eventList.add("['SET(S2/k2 = v2b)']");

		LatchedListener l = new LatchedListener(latch) {
			@Override
			public void check(ConfigEvents events) throws Exception {
				assertObjectEquals(eventList.poll(), events);
			}
		};

		ConfigMap cm = s.getMap("Foo.cfg");
		cm.register(l);
		cm.setEntry("S2", "k2", "v2b", null, null, null);
		s.update("Foo.cfg",
			"[S1]",
			"k1 = v1b"
		);
		cm.commit();
		wait(latch);
		assertNull(l.error);
		cm.unregister(l);

		assertTextEquals("[S1]|k1 = v1b|[S2]|k2 = v2b|", cm.toString());
	}

	//-----------------------------------------------------------------------------------------------------------------
	// If we're modifying an entry and it changes on the file system, we should overwrite the change on save().
	//-----------------------------------------------------------------------------------------------------------------

	@Test
	public void testMergeWithOverwrite() throws Exception {
		ConfigStore s = initStore("Foo.cfg",
			"[S1]",
			"k1 = v1a"
		);

		final CountDownLatch latch = new CountDownLatch(2);
		final Queue<String> eventList = new ConcurrentLinkedQueue<>();
		eventList.add("['SET(S1/k1 = v1b)']");
		eventList.add("['SET(S1/k1 = v1c)']");

		LatchedListener l = new LatchedListener(latch) {
			@Override
			public void check(ConfigEvents events) throws Exception {
				assertObjectEquals(eventList.poll(), events);
			}
		};

		ConfigMap cm = s.getMap("Foo.cfg");
		cm.register(l);
		cm.setEntry("S1", "k1", "v1c", null, null, null);
		s.update("Foo.cfg",
			"[S1]",
			"k1 = v1b"
		);
		cm.commit();
		wait(latch);
		assertNull(l.error);
		cm.unregister(l);

		assertTextEquals("[S1]|k1 = v1c|", cm.toString());
	}

	//-----------------------------------------------------------------------------------------------------------------
	// If the contents of a file have been modified on the file system before a signal has been received.
	//-----------------------------------------------------------------------------------------------------------------

	@Test
	public void testMergeWithOverwriteNoSignal() throws Exception {

		final Queue<String> contents = new ConcurrentLinkedQueue<>();
		contents.add("[S1]\nk1 = v1a");
		contents.add("[S1]\nk1 = v1b");
		contents.add("[S1]\nk1 = v1c");
		contents.add("[S1]\nk1 = v1c");

		ConfigMemoryStore s = new ConfigMemoryStore(null) {
			@Override
			public synchronized String read(String name) {
				return contents.poll();
			}
		};
		try {
			final CountDownLatch latch = new CountDownLatch(2);
			final Queue<String> eventList = new ConcurrentLinkedQueue<>();
			eventList.add("['SET(S1/k1 = v1b)']");
			eventList.add("['SET(S1/k1 = v1c)']");

			LatchedListener l = new LatchedListener(latch) {
				@Override
				public void check(ConfigEvents events) throws Exception {
					assertObjectEquals(eventList.poll(), events);
				}
			};

			ConfigMap cm = s.getMap("Foo.cfg");
			cm.register(l);
			cm.setEntry("S1", "k1", "v1c", null, null, null);
			cm.commit();
			wait(latch);
			assertNull(l.error);
			cm.unregister(l);

			assertTextEquals("[S1]|k1 = v1c|", cm.toString());

		} finally {
			s.close();
		}
	}

	@Test
	public void testMergeWithConstantlyUpdatingFile() throws Exception {

		ConfigMemoryStore s = new ConfigMemoryStore(null) {
			char c = 'a';
			@Override
			public synchronized String read(String name) {
				return "[S1]\nk1 = v1" + (c++);
			}
		};
		try {
			final CountDownLatch latch = new CountDownLatch(10);
			final Queue<String> eventList = new ConcurrentLinkedQueue<>();
			eventList.add("['SET(S1/k1 = v1b)']");
			eventList.add("['SET(S1/k1 = v1c)']");
			eventList.add("['SET(S1/k1 = v1d)']");
			eventList.add("['SET(S1/k1 = v1e)']");
			eventList.add("['SET(S1/k1 = v1f)']");
			eventList.add("['SET(S1/k1 = v1g)']");
			eventList.add("['SET(S1/k1 = v1h)']");
			eventList.add("['SET(S1/k1 = v1i)']");
			eventList.add("['SET(S1/k1 = v1j)']");
			eventList.add("['SET(S1/k1 = v1k)']");

			LatchedListener l = new LatchedListener(latch) {
				@Override
				public void check(ConfigEvents events) throws Exception {
					assertObjectEquals(eventList.poll(), events);
				}
			};

			ConfigMap cm = s.getMap("Foo.cfg");
			cm.register(l);
			cm.setEntry("S1", "k1", "v1c", null, null, null);
			try {
				cm.commit();
				fail("Exception expected.");
			} catch (ConfigException e) {
				assertEquals("Unable to store contents of config to store.", e.getMessage());
			}
			wait(latch);
			assertNull(l.error);
			cm.unregister(l);

			assertTextEquals("[S1]|k1 = v1c|", cm.toString());

		} finally {
			s.close();
		}
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Utilities.
	//-----------------------------------------------------------------------------------------------------------------

	private static ConfigStore initStore(String name, String...contents) {
		return ConfigMemoryStore.create().build().update(name, contents);
	}

	public static class LatchedListener implements ConfigEventListener {
		private final CountDownLatch latch;
		private volatile String error = null;
		public LatchedListener(CountDownLatch latch) {
			this.latch = latch;
		}

		@Override
		public void onConfigChange(ConfigEvents events) {
			try {
				check(events);
			} catch (Exception e) {
				error = e.getLocalizedMessage();
			}
			for (int i = 0; i < events.size(); i++)
				latch.countDown();
		}

		public void check(ConfigEvents events) throws Exception {
		}
	}

	private static void wait(CountDownLatch latch) throws InterruptedException {
		if (! latch.await(10, TimeUnit.SECONDS))
			throw new RuntimeException("Latch failed.");
	}
}
