package io.activej.trigger;

import io.activej.common.ref.Ref;
import io.activej.common.ref.RefBoolean;
import io.activej.eventloop.Eventloop;
import io.activej.test.time.TestCurrentTimeProvider;
import io.activej.trigger.Triggers.TriggerWithResult;
import org.junit.Before;
import org.junit.Test;

import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static io.activej.trigger.Severity.HIGH;
import static org.junit.Assert.*;

public class TriggersTest {

	private Triggers triggers;
	private long timestamp;

	@Before
	public void setUp() {
		triggers = Triggers.builder()
			.withTrigger(Severity.HIGH, "Component", "nameOne", TriggerResult::create)
			.withTrigger(Severity.WARNING, "AnotherComponent", "nameTwo", TriggerResult::create)
			.withTrigger(Severity.DEBUG, "AnotherComponent", "name", TriggerResult::create)
			.withTrigger(Severity.DISASTER, "Component", "name", TriggerResult::create)
			.withTrigger(Severity.HIGH, "Component", "name", TriggerResult::create)
			.withTrigger(Severity.WARNING, "Component", "nameThree", TriggerResult::create)
			.withTrigger(Severity.HIGH, "ComponentName", "name", TriggerResult::create)
			.withTrigger(Severity.HIGH, "Component", "nameTwo", TriggerResult::create)
			.withTrigger(Severity.DISASTER, "AnotherComponent", "name", TriggerResult::create)
			.withTrigger(Severity.WARNING, "Component", "nameOne", TriggerResult::create)
			.withTrigger(Severity.INFORMATION, "Component", "name", TriggerResult::create)
			.build();
		timestamp = 10000;
	}

	@Test
	public void testAddTriggers() {
		triggers = Triggers.builder()
			.withTrigger(Severity.HIGH, "Component", "Name", TriggerResult::none)
			.build();
		List<String> triggers = this.triggers.getTriggers();
		assertEquals(1, triggers.size());
	}

	@Test
	public void testSuppressAllTriggers() {
		assertEquals(7, triggers.getResults().size());
		triggers.suppressAllTriggers();
		assertEquals(0, triggers.getResults().size());
	}

	@Test
	public void testDuplicateTriggers() {
		triggers.addTrigger(Severity.HIGH, "Component", "nameOne", TriggerResult::create);
		triggers.suppressTriggerByName("nameOne");
		triggers.getResults().forEach(result -> assertNotEquals("nameOne", result.trigger().getName()));
	}

	@Test
	public void testDuplicateTriggersResume() {
		RefBoolean condition = new RefBoolean(true);
		triggers = Triggers.builder()
			.withTrigger(Severity.HIGH, "Component", "nameOne", TriggerResult::create)
			.withTrigger(Severity.HIGH, "Component", "nameOne", TriggerResult::create)
			.withTrigger(Severity.HIGH, "Component", "nameOne", () -> {
				if (!condition.get()) {
					return TriggerResult.none();
				}
				return TriggerResult.create();
			})
			.build();
		triggers.now = TestCurrentTimeProvider.ofTimeSequence(System.currentTimeMillis(), Triggers.CACHE_TIMEOUT.toMillis());

		assertEquals(3, triggers.getResults().size());
		triggers.suppressTriggerByName("nameOne");
		assertEquals(0, triggers.getResults().size());
		condition.set(false);
		triggers.now = TestCurrentTimeProvider.ofTimeSequence(triggers.now.currentTimeMillis() + 10, 10);
		triggers.getResults();
		condition.set(true);
		triggers.now = TestCurrentTimeProvider.ofTimeSequence(triggers.now.currentTimeMillis() + 1000, 10);
		assertEquals(1, triggers.getResults().size());
	}

	@Test
	public void testResumeTrigger() {
		RefBoolean condition = new RefBoolean(true);
		triggers = Triggers.builder()
			.withTrigger(Severity.HIGH, "Component", "nameOne", () -> {
				if (!condition.get()) {
					return TriggerResult.none();
				}
				return TriggerResult.create();
			})
			.build();
		triggers.now = TestCurrentTimeProvider.ofTimeSequence(System.currentTimeMillis(), Triggers.CACHE_TIMEOUT.toMillis());
		List<TriggerWithResult> results = triggers.getResults();
		assertEquals(1, results.size());
		triggers.suppressTriggerByName("nameOne");
		assertEquals(0, triggers.getResults().size());
		condition.set(false);
		triggers.now = TestCurrentTimeProvider.ofTimeSequence(triggers.now.currentTimeMillis() + 10, 10);
		triggers.getResults();
		condition.set(true);
		triggers.now = TestCurrentTimeProvider.ofTimeSequence(triggers.now.currentTimeMillis() + 1000, 10);
		assertEquals(1, triggers.getResults().size());
	}

	@Test
	public void testSuppressByName() {
		assertEquals(11, triggers.getTriggers().size());
		triggers.suppressTriggerByName("nameOne");
		triggers.getResults().forEach(result -> assertNotEquals("nameOne", result.trigger().getName()));
	}

	@Test
	public void testSuppressByComponent() {
		assertEquals(11, triggers.getTriggers().size());
		triggers.suppressTriggerByComponent("Component");
		triggers.getResults().forEach(result -> assertNotEquals("Component", result.trigger().getComponent()));
	}

	@Test
	public void testSuppressBySeverity() {
		assertEquals(11, triggers.getTriggers().size());
		triggers.suppressTriggerBySeverity("HIGH");
		triggers.getResults().forEach(result -> assertNotEquals("nameOne", result.trigger().getSeverity().name()));
	}

	@Test
	public void testTriggersSuppression() {
		initializeTriggers();

		triggers.suppressTriggerByName("fatalErrors");
		triggers.suppressTriggerByName("delay");
		triggers.suppressTriggerByName("error");
		triggers.suppressTriggerByName("errors");
		triggers.suppressTriggerByName("errorProcessLogs");
		triggers.suppressTriggerByName("runDelay");

		assertTrue(triggers.getResults().isEmpty());
	}

	@Test
	public void testSuppressBySignature() {
		initializeTriggers();

		triggers.suppressTriggersBySignature("HIGH : @CubeThread Eventloop : fatalErrors");
		triggers.suppressTriggersBySignature("HIGH : @Named(\"CubeFetchScheduler\") EventloopTaskScheduler : delay");
		triggers.suppressTriggersBySignature("HIGH : @Named(\"CubeFetchScheduler\") EventloopTaskScheduler : error");
		triggers.suppressTriggersBySignature("HIGH : @Named(\"LogProcessorScheduler\") EventloopTaskScheduler : delay");
		triggers.suppressTriggersBySignature("HIGH : @Named(\"LogProcessorScheduler\") EventloopTaskScheduler : error");
		triggers.suppressTriggersBySignature("HIGH : Cube : errors");
		triggers.suppressTriggersBySignature("AVERAGE : CubeLogProcessorController : errorProcessLogs");
		triggers.suppressTriggersBySignature("AVERAGE : Launcher : runDelay");
		triggers.suppressTriggersBySignature("WARNING : @Named(\"CubeFetchScheduler\") EventloopTaskScheduler : delay");
		triggers.suppressTriggersBySignature("WARNING : @Named(\"CubeFetchScheduler\") EventloopTaskScheduler : error");
		triggers.suppressTriggersBySignature("WARNING : @Named(\"LogProcessorScheduler\") EventloopTaskScheduler : delay");
		triggers.suppressTriggersBySignature("WARNING : @Named(\"LogProcessorScheduler\") EventloopTaskScheduler : error");

		assertTrue(triggers.getResults().isEmpty());
	}

	@Test
	public void testGetMultilineSuppressedResults() {
		initializeTriggers();
		triggers.suppressTriggersBySignature("HIGH : @CubeThread Eventloop : fatalErrors");
		triggers.suppressTriggersBySignature("HIGH : @Named(\"CubeFetchScheduler\") EventloopTaskScheduler : delay");
		triggers.suppressTriggersBySignature("HIGH : @Named(\"CubeFetchScheduler\") EventloopTaskScheduler : error");

		assertEquals("""
				HIGH : @CubeThread Eventloop : fatalErrors
				HIGH : @Named("CubeFetchScheduler") EventloopTaskScheduler : delay
				HIGH : @Named("CubeFetchScheduler") EventloopTaskScheduler : error""",
			triggers.getMultilineSuppressedResults());

		String resultsAfterSuppression = triggers.getMultilineResults();
		Arrays.stream(triggers.getMultilineSuppressedResults().split("\n"))
			.forEach(suppressed -> assertFalse(resultsAfterSuppression.contains(suppressed)));
	}

	@Test
	public void testWithoutTimestamp() {
		triggers = Triggers.builder()
			.withTrigger(HIGH, Eventloop.class.getName(), "ProcessDelay", TriggerResult::create)
			.build();
		triggers.now = TestCurrentTimeProvider.ofTimeSequence(timestamp, Triggers.CACHE_TIMEOUT.toMillis() + 1);

		long currentTimestamp = timestamp;
		List<TriggerWithResult> results = triggers.getResults();
		assertEquals(1, results.size());
		assertEquals(currentTimestamp, results.get(0).triggerResult().getTimestamp());

		results = triggers.getResults();
		assertEquals(1, results.size());
		assertEquals(currentTimestamp, results.get(0).triggerResult().getTimestamp());
	}

	@Test
	public void testOfTimestamp() {
		triggers = Triggers.builder()
			.withTrigger(HIGH, Eventloop.class.getName(), "ProcessDelay", () -> TriggerResult.ofTimestamp(increaseTimestampAndGet()))
			.build();
		triggers.now = TestCurrentTimeProvider.ofTimeSequence(timestamp, Triggers.CACHE_TIMEOUT.toMillis() + 1);

		long currentTimestamp = timestamp;
		List<TriggerWithResult> results = triggers.getResults();
		assertEquals(1, results.size());
		assertEquals(currentTimestamp + 10000, results.get(0).triggerResult().getTimestamp());

		results = triggers.getResults();
		assertEquals(1, results.size());
		assertEquals(currentTimestamp + 20000, results.get(0).triggerResult().getTimestamp());
	}

	@Test
	public void testOfInstant() {
		triggers = Triggers.builder()
			.withTrigger(HIGH, Eventloop.class.getName(), "ProcessDelay", () -> TriggerResult.ofInstant(Instant.ofEpochMilli(increaseTimestampAndGet())))
			.build();
		triggers.now = TestCurrentTimeProvider.ofTimeSequence(timestamp, Triggers.CACHE_TIMEOUT.toMillis() + 1);

		long currentTimestamp = timestamp;
		List<TriggerWithResult> results = triggers.getResults();
		assertEquals(1, results.size());
		assertEquals(currentTimestamp + 10000, results.get(0).triggerResult().getTimestamp());

		results = triggers.getResults();
		assertEquals(1, results.size());
		assertEquals(currentTimestamp + 20000, results.get(0).triggerResult().getTimestamp());
	}

	@Test
	public void testOfValueWithPredicate() {
		triggers = Triggers.builder()
			.withTrigger(HIGH, Eventloop.class.getName(), "ProcessDelay", () -> TriggerResult.ofValue(increaseTimestampAndGet(), time -> time > 1000))
			.build();
		int increment = 100;
		triggers.now = TestCurrentTimeProvider.ofTimeSequence(timestamp, Triggers.CACHE_TIMEOUT.toMillis() + increment);

		long currentTimestamp = timestamp;
		List<TriggerWithResult> results = triggers.getResults();

		assertEquals(1, results.size());
		assertEquals(currentTimestamp, results.get(0).triggerResult().getTimestamp());
		assertEquals(currentTimestamp + 10000, results.get(0).triggerResult().getValue());

		results = triggers.getResults();
		assertEquals(1, results.size());
		assertEquals(currentTimestamp, results.get(0).triggerResult().getTimestamp());
		assertEquals(currentTimestamp + 20000, results.get(0).triggerResult().getValue());
	}

	@Test
	public void testConcurrentAccess() throws InterruptedException {
		triggers.now = TestCurrentTimeProvider.ofTimeSequence(100, Triggers.CACHE_TIMEOUT.toMillis() * 2);
		int nThreads = 10;
		List<Thread> threads = new ArrayList<>(nThreads);
		Ref<Throwable> throwableRef = new Ref<>();
		for (int i = 0; i < nThreads; i++) {
			threads.add(new Thread(() -> {
				try {
					for (int j = 0; j < 1_000; j++) {
						if (throwableRef.get() != null) {
							break;
						}
						triggers.getMultilineResultsHigh();
					}
				} catch (Throwable e) {
					throwableRef.set(e);
				}
			}));
		}

		threads.forEach(Thread::start);
		for (Thread thread : threads) {
			thread.join();
		}
		assertNull(throwableRef.get());
	}

	@Test
	public void testEmptyNewResults() {
		Triggers triggers = Triggers.create();
		triggers.now = TestCurrentTimeProvider.ofTimeSequence(10_000, Triggers.CACHE_TIMEOUT.toMillis() * 2);

		for (int i = 0; i < 10; i++) {
			RefBoolean s = new RefBoolean(false);
			triggers.addTrigger(Severity.HIGH, "Component", "name" + i, () -> s.flip() ?
				TriggerResult.create() :
				TriggerResult.none());
		}

		assertEquals(10, triggers.getResultsHigh().size());
		assertTrue(triggers.getResultsHigh().isEmpty());
	}

	private long increaseTimestampAndGet() {
		timestamp += 10000;
		return timestamp;
	}

	private void initializeTriggers() {
		triggers = Triggers.builder()
			.withTrigger(Severity.HIGH, "@CubeThread Eventloop", "fatalErrors", TriggerResult::create)
			.withTrigger(Severity.HIGH, "@Named(\"CubeFetchScheduler\") EventloopTaskScheduler", "delay", TriggerResult::create)
			.withTrigger(Severity.HIGH, "@Named(\"CubeFetchScheduler\") EventloopTaskScheduler", "error", TriggerResult::create)
			.withTrigger(Severity.HIGH, "@Named(\"LogProcessorScheduler\") EventloopTaskScheduler", "delay", TriggerResult::create)
			.withTrigger(Severity.HIGH, "@Named(\"LogProcessorScheduler\") EventloopTaskScheduler", "error", TriggerResult::create)
			.withTrigger(Severity.HIGH, "Cube", "errors", TriggerResult::create)
			.withTrigger(Severity.AVERAGE, "CubeLogProcessorController", "errorProcessLogs", TriggerResult::create)
			.withTrigger(Severity.AVERAGE, "Launcher", "runDelay", TriggerResult::create)
			.withTrigger(Severity.WARNING, "@Named(\"CubeFetchScheduler\") EventloopTaskScheduler", "delay", TriggerResult::create)
			.withTrigger(Severity.WARNING, "@Named(\"CubeFetchScheduler\") EventloopTaskScheduler", "error", TriggerResult::create)
			.withTrigger(Severity.WARNING, "@Named(\"LogProcessorScheduler\") EventloopTaskScheduler", "delay", TriggerResult::create)
			.withTrigger(Severity.WARNING, "@Named(\"LogProcessorScheduler\") EventloopTaskScheduler", "error", TriggerResult::create)
			.build();
		assertEquals(
			"@CubeThread Eventloop, " +
			"@Named(\"CubeFetchScheduler\") EventloopTaskScheduler, " +
			"@Named(\"LogProcessorScheduler\") EventloopTaskScheduler, " +
			"Cube, " +
			"CubeLogProcessorController, " +
			"Launcher",
			triggers.getTriggerComponents());
		assertEquals(
			"[" +
			"@CubeThread Eventloop : fatalErrors, " +
			"@Named(\"CubeFetchScheduler\") EventloopTaskScheduler : delay, " +
			"@Named(\"CubeFetchScheduler\") EventloopTaskScheduler : error, " +
			"@Named(\"LogProcessorScheduler\") EventloopTaskScheduler : delay, " +
			"@Named(\"LogProcessorScheduler\") EventloopTaskScheduler : error, " +
			"Cube : errors, " +
			"CubeLogProcessorController : errorProcessLogs, " +
			"Launcher : runDelay]",
			triggers.getTriggerNames().toString());
		assertEquals(
			"[" +
			"HIGH : @CubeThread Eventloop : fatalErrors, " +
			"HIGH : @Named(\"CubeFetchScheduler\") EventloopTaskScheduler : delay, " +
			"HIGH : @Named(\"CubeFetchScheduler\") EventloopTaskScheduler : error, " +
			"HIGH : @Named(\"LogProcessorScheduler\") EventloopTaskScheduler : delay, " +
			"HIGH : @Named(\"LogProcessorScheduler\") EventloopTaskScheduler : error, " +
			"HIGH : Cube : errors, " +
			"AVERAGE : CubeLogProcessorController : errorProcessLogs, " +
			"AVERAGE : Launcher : runDelay, " +
			"WARNING : @Named(\"CubeFetchScheduler\") EventloopTaskScheduler : delay, " +
			"WARNING : @Named(\"CubeFetchScheduler\") EventloopTaskScheduler : error, " +
			"WARNING : @Named(\"LogProcessorScheduler\") EventloopTaskScheduler : delay, " +
			"WARNING : @Named(\"LogProcessorScheduler\") EventloopTaskScheduler : error]",
			triggers.getTriggers().toString());
		assertEquals(8, triggers.getResults().size());
	}
}
