package word;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class LazyValueTest {
  @Nested
  public class Q1 {
    @Test
    public void testExample() {
      var lazyValue = new LazyValue<>(() -> 42);

      assertEquals(42, lazyValue.value());
    }

    @Test
    public void testSeveralInstances() {
      var lazyValue1 = new LazyValue<>(() -> 5);
      var lazyValue2 = new LazyValue<>(() -> 7);

      assertEquals(5, lazyValue1.value());
      assertEquals(7, lazyValue2.value());
    }

    @Test
    public void testSingleThreadedInitialization() {
      // Test that value is initialized correctly in a single thread
      var counter = new AtomicInteger(0);
      var lazyValue = new LazyValue<>(counter::incrementAndGet);

      assertEquals(1, lazyValue.value());
      assertEquals(1, lazyValue.value()); // Should still be 1, not incremented again
      assertEquals(1, counter.get()); // Supplier should only have been called once
    }

    @RepeatedTest(20) // Run multiple times to increase chance of detecting race conditions
    public void testHighContentionScenario() throws Exception {
      var threadCount = 100;

      // This supplier may take a long time
      var expensiveOperation = (Supplier<String>) () -> {
        try {
          Thread.sleep((long) (Math.random() * 5));
        } catch (InterruptedException e) {
          throw new AssertionError(e);
        }
        return "Computed Value";
      };

      var lazyValue = new LazyValue<>(expensiveOperation);
      var readyLatch = new CountDownLatch(threadCount);
      var startLatch = new CountDownLatch(1);
      var results = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());

      try (var executor = Executors.newFixedThreadPool(threadCount)) {

        // Launch threads that will all try to get the value simultaneously
        for (int i = 0; i < threadCount; i++) {
          executor.submit(() -> {
            try {
              readyLatch.countDown();
              startLatch.await(); // Wait for the signal to start
            } catch (InterruptedException e) {
              throw new AssertionError(e);
            }

            var value = lazyValue.value();
            results.add(value);
          });
        }

        // Wait for all threads to be ready
        readyLatch.await();
        // Start all tasks at once
        startLatch.countDown();

        // Shut down the executor and wait for all tasks to complete
      }

      // Verify that all threads got the same value
      assertEquals(1, results.size(), "All threads should see the same value");

      // Call value again to ensure it returns the same instance
      var finalValue = lazyValue.value();
      assertSame(results.iterator().next(), finalValue, "Value should be consistent on future calls");
    }

    @Test
    public void testNullSupplierValue() {
      var lazyValue = new LazyValue<>(() -> null);

      assertThrows(NullPointerException.class, lazyValue::value);
    }

    @Test
    public void testConstructorNullCheck() {
      assertThrows(NullPointerException.class, () -> new LazyValue<>(null));
    }
  }


  @Nested
  public class Q2 {

    @Test
    public void testExample() {
      var lazyValue = new LazyOnceValue<>(() -> 42);
      assertEquals(42, lazyValue.value());
    }

    @Test
    public void testSeveralInstances() {
      var lazyValue1 = new LazyOnceValue<>(() -> 5);
      var lazyValue2 = new LazyOnceValue<>(() -> 7);

      assertEquals(5, lazyValue1.value());
      assertEquals(7, lazyValue2.value());
    }

    @Test
    public void testSingleThreadedInitialization() {
      // Test that value is initialized correctly in a single thread
      var counter = new AtomicInteger(0);
      var lazyValue = new LazyOnceValue<>(counter::incrementAndGet);

      assertEquals(1, lazyValue.value());
      assertEquals(1, lazyValue.value()); // Should still be 1, not incremented again
      assertEquals(1, counter.get()); // Supplier should only have been called once
    }

    @RepeatedTest(20) // Run multiple times to increase chance of detecting race conditions
    public void testMultiThreadedInitialization() throws Exception {
      final int threadCount = 100;
      var counter = new AtomicInteger(0);

      var lazyValue = new LazyOnceValue<>(() -> {
        // Simulate some work
        try {
          Thread.sleep(10);
        } catch (InterruptedException e) {
          throw new AssertionError(e);
        }
        return counter.incrementAndGet();
      });

      var readyLatch = new CountDownLatch(threadCount);
      var startLatch = new CountDownLatch(1);

      try (var executor = Executors.newFixedThreadPool(threadCount)) {

        // Launch threads that will all try to get the value simultaneously
        for (int i = 0; i < threadCount; i++) {
          executor.submit(() -> {
            try {
              readyLatch.countDown();
              startLatch.await(); // Wait for the signal to start
            } catch (InterruptedException e) {
              throw new AssertionError(e);
            }

            var _ = lazyValue.value();
          });
        }

        // Wait for all threads to be ready
        readyLatch.await();
        // Start all tasks at once
        startLatch.countDown();

        // Shut down the executor and wait for all tasks to complete
      }

      // The supplier should only have been called once
      assertEquals(1, counter.get(), "Supplier should only be called once");
    }

    @Test
    public void testNullSupplierValue() {
      var lazyValue = new LazyOnceValue<>(() -> null);

      assertThrows(NullPointerException.class, lazyValue::value);
    }

    @Test
    public void testConstructorNullCheck() {
      assertThrows(NullPointerException.class, () -> new LazyOnceValue<>(null));
    }
  }


  @Nested
  public class Q3 {

    @Test
    public void constructorShouldInitializeWithCorrectSize() {
      var list = new LazyOnceList<>(5, index -> "Value " + index);

      assertEquals(5, list.size());
      assertEquals("Value 3", list.get(3));
    }

    @Test
    public void constructorShouldThrowNullPointerExceptionWhenFunctionIsNull() {
      assertThrows(NullPointerException.class, () -> new LazyOnceList<>(5, null));
    }

    @Test
    public void getShouldRetrieveValuesLazily() {
      var functionCallCount = new AtomicInteger(0);
      var list = new LazyOnceList<>(5, index -> {
        functionCallCount.incrementAndGet();
        return "Value " + index;
      });

      assertEquals(0, functionCallCount.get(), "Function should not be called before get()");

      var value0 = list.get(0);
      assertEquals("Value 0", value0);
      assertEquals(1, functionCallCount.get(), "Function should be called once");

      var value2 = list.get(2);
      assertEquals("Value 2", value2);
      assertEquals(2, functionCallCount.get(), "Function should be called twice");

      // Call get() on same index again
      var value0Again = list.get(0);
      assertEquals("Value 0", value0Again);
      assertEquals(2, functionCallCount.get(), "Function should not be called again for previously retrieved index");
    }

    @Test
    public void getShouldThrowIndexOutOfBoundsExceptionForInvalidIndex() {
      var list = new LazyOnceList<>(3, index -> "Value " + index);

      assertThrows(IndexOutOfBoundsException.class, () -> list.get(-1));
      assertThrows(IndexOutOfBoundsException.class, () -> list.get(3));
    }

    @Test
    public void getShouldThrowNullPointerExceptionWhenFunctionReturnsNull() {
      var list = new LazyOnceList<>(3, index -> null);

      assertThrows(NullPointerException.class, list::getFirst);
    }

    @Test
    public void lazyOnceListShouldWorkAsList() {
      List<String> list = new LazyOnceList<>(3, index -> "Value " + index);

      assertEquals(3, list.size());
      assertEquals("Value 0", list.get(0));
      assertEquals("Value 1", list.get(1));
      assertEquals("Value 2", list.get(2));
    }

    @Test
    public void lazyOnceListShouldWorkWithSmallSize() {
      var list = new LazyOnceList<>(1, index -> "Value " + index);

      assertEquals(1, list.size());
      assertEquals("Value 0", list.get(0));
    }

    @Test
    public void lazyOnceListShouldWorkWithMediumSize() {
      var list = new LazyOnceList<>(10, index -> "Value " + index);

      assertEquals(10, list.size());
      assertEquals("Value 9", list.get(9));
    }

    @Test
    public void lazyOnceListShouldWorkWithLargeSize() {
      var list = new LazyOnceList<>(100, index -> "Value " + index);

      assertEquals(100, list.size());
      assertEquals("Value 99", list.get(99));
    }

    @RepeatedTest(20) // Run multiple times to increase chance of detecting race conditions
    public void lazyOnceListShouldCallFunctionOnlyOnceInConcurrentEnvironment() throws InterruptedException {
      var size = 10;
      var threads = 5;
      var index = 3;  // We'll test concurrent access to this index

      var slowFunctionCallCount = new AtomicInteger(0);
      var list = new LazyOnceList<>(size, index2 -> {
        slowFunctionCallCount.incrementAndGet();
        try {
          // Simulate some work
          Thread.sleep(100);
        } catch (InterruptedException e) {
          throw new AssertionError(e);
        }
        return "Value " + index2;
      });
      try (var executorService = Executors.newFixedThreadPool(threads)) {
        var latch = new CountDownLatch(threads);

        // Have multiple threads request the same index concurrently
        for (int i = 0; i < threads; i++) {
          executorService.submit(() -> {
            try {
              var value = list.get(index);
              assertEquals("Value " + index, value);
            } finally {
              latch.countDown();
            }
          });
        }

        // Wait for all threads to complete
        var allThreadsCompleted = latch.await(5, TimeUnit.SECONDS);
        assertTrue(allThreadsCompleted, "All threads should have completed");
      }


      assertEquals(1, slowFunctionCallCount.get(),
          "Function should be called exactly once even with concurrent access");
    }
  }
}