/*
 * RiskScape™ Copyright New Zealand Institute for Earth Science Limited
 * (Earth Sciences New Zealand) is distributed for research purposes only
 * under the terms of AGPLv3.
 *
 * RiskScape™ Copyright 2026 New Zealand Institute for Earth Science
 * Limited (Earth Sciences New Zealand). All rights reserved. Source code
 * available under the AGPLv3.
 * 
 * This program is free software: you can redistribute it and/or modify it under
 *  the terms of the GNU Affero General Public License as published by the Free
 *  Software Foundation, either version 3 of the License, or (at your option) any
 *  later version.
 * 
 * This program is distributed for RESEARCH PURPOSES ONLY, in the hope that it will
 * be useful for research and education initiatives.
 * 
 * If you are not a researcher, or you are a researcher who wishes to use this
 * program on terms other than AGPLv3 (including those who wish to restrict the
 * distribution of any source code created using this program), please contact:
 * https://riskscape.org.nz
 * 
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.  You should have received a copy
 * of the GNU Affero General Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * By way of summary only, under the AGPLv3:
 *     • Permissions of this strongest copyleft license are conditioned
 *       on making available complete source code of licensed works and
 *       modifications, which include larger works using a licensed work,
 *       under the same license.
 *     • Copyright and license notices must be preserved.
 *     • Contributors provide an express grant of patent rights.
 *     • When a modified version is used to provide a service over a
 *       network, the complete source code of the modified version must be made
 *       available.
 */
package nz.org.riskscape.defaults;

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.junit.Test;

import nz.org.riskscape.engine.ProjectTest;
import nz.org.riskscape.engine.types.Nullable;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Types;

public class RealizationContextTest extends ProjectTest {

  @Test
  public void canNormalizeStructsToReturnACanonicalInstance() {
    // we used to test empty struct for completeness, but it's pretty much impossible to create one now
    // except using the singleton

    // ok, now with some members, just to prove it's based on equality
    Struct fooInt = Struct.of("foo", Types.INTEGER);
    assertSame(fooInt, realizationContext.normalizeStruct(fooInt));
    assertSame(fooInt, realizationContext.normalizeStruct(Struct.of("foo", Types.INTEGER)));

    Struct fooText = Struct.of("foo", Types.TEXT);
    // based on struct equality, not member names
    assertNotSame(fooInt, realizationContext.normalizeStruct(fooText));
    assertSame(fooText, realizationContext.normalizeStruct(Struct.of("foo", Types.TEXT)));
  }

  @Test
  public void aStructWithNonIdenticalChildStructsIsNotNormalized() throws Exception {
    // without this behaviour, a struct with nested structs can be swapped out for one with different nested structs
    // which can cause a realized expression to have a struct mismatch
    // NB - type 'normalization' needs to be considered a bit more and decided whether it's the right approach, or
    // instead drop the owner check from tuples and rely more on integration and unit tests to find mistakes
    Struct foo1 = Struct.of("foo", Types.TEXT);
    Struct foo2 = Struct.of("foo", Types.TEXT);

    Struct parent1 = Struct.of("bar", foo1);
    Struct parent2 = Struct.of("bar", foo2);

    assertSame(parent1, realizationContext.normalizeStruct(parent1));
    assertEquals(parent1, parent2);
    assertNotSame(parent1, realizationContext.normalizeStruct(parent2));

    Struct parent1Too = Struct.of("bar", foo1);
    assertNotSame(parent1Too, parent1);
    assertEquals(parent1Too, parent1);
    assertSame(parent1, realizationContext.normalizeStruct(parent1Too));
  }

  @Test
  public void canNormalizeStructWithNullableMember() {
    // First a nullable struct attribute
    Struct childStruct = Struct.of("value", Types.INTEGER);
    Struct s1 = Struct.of("a", Types.INTEGER, "b", Nullable.of(childStruct));

    assertSame(s1, realizationContext.normalizeStruct(s1));

    assertSame(s1, realizationContext.normalizeStruct(Struct.of("a", Types.INTEGER, "b", Nullable.of(childStruct))));

    // in this case, the child, b, is not nullable, so it's not the same and so shouldn't normalize to s1
    Struct s2 = Struct.of("a", Types.INTEGER, "b", childStruct);
    assertNotSame(s1, s2);
    assertSame(s2, realizationContext.normalizeStruct(s2));

    assertSame(s1, realizationContext.normalizeStruct(Struct.of("a", Types.INTEGER, "b", Nullable.of(childStruct))));

    // now we try with a nullable simple type
    Struct s3 = Struct.of("a", Types.INTEGER, "b", Nullable.of(Types.INTEGER));
    assertSame(s3, realizationContext.normalizeStruct(s3));

    assertSame(s3, realizationContext.normalizeStruct(Struct.of("a", Types.INTEGER, "b", Nullable.of(Types.INTEGER))));
  }

  @Test
  public void cacheOnlyComputesWhenMissing() {
    AtomicInteger counter = new AtomicInteger();
    Function<Object, String> fooCompute = (key) -> String.format("foo%d", counter.getAndIncrement());
    Function<Object, String> barCompute = (key) -> String.format("bar%d", counter.getAndIncrement());

    for (int i = 0; i < 1000; i++) {
      assertThat(
          realizationContext.getOrComputeFromCache(RealizationContextTest.class, "foo", String.class, fooCompute),
          is("foo0"));
    }

    for (int i = 0; i < 1000; i++) {
      assertThat(
          realizationContext.getOrComputeFromCache(RealizationContextTest.class, "bar", String.class, barCompute),
          is("bar1"));
    }

    for (int i = 0; i < 1000; i++) {
      assertThat(
          realizationContext.getOrComputeFromCache(RealizationContextTest.class, "foo", String.class, fooCompute),
          is("foo0"));
    }

    // Use up all the free memory. This should cause cache entries to be purged.
    allocateAllFreeMemory();
    for (int i = 0; i < 1000; i++) {
      assertThat(
          realizationContext.getOrComputeFromCache(RealizationContextTest.class, "foo", String.class, fooCompute),
          is("foo2")); // yep the increment in the count proves that entry is recomputed
    }
  }

  @Test
  public void cacheHandlesConcurrentAccess() throws Exception {
    // in this test we make a bunch of callable tasks that will all be trying to access the same cache key,
    // they should be trying this concurrently since they are thrown at an executor service with many threads.
    AtomicInteger fooCounter = new AtomicInteger();
    Function<Object, String> fooCompute = (key) -> String.format("foo%d", fooCounter.getAndIncrement());

    List<Callable<String>> tasks = IntStream.range(0, 1000)
        .<Callable<String>>mapToObj(i ->  () -> realizationContext.getOrComputeFromCache(
              RealizationContextTest.class, "foo-concurrent", String.class, fooCompute)
        ).collect(Collectors.toList());

    ExecutorService executor = Executors.newFixedThreadPool(20);
    List<Future<String>> futures = executor.invokeAll(tasks);
    while (futures.stream().anyMatch(f -> ! f.isDone())) {
      Thread.sleep(100);
    }
    // the fooCounter should only have been incremented once, because only the first callable should have
    // caused the cache to compute the value
    assertEquals(1, fooCounter.get());

    // and all futures should get the same value from the cache
    for (Future<String> future: futures) {
      assertEquals("foo0", future.get());
    }
  }

  private void allocateAllFreeMemory() {
    List<Object[]> memoryHog = new ArrayList<>();
    try {
      long freeMemory;
      while ((freeMemory = Runtime.getRuntime().freeMemory()) > 0) {
        memoryHog.add(new Object[(int)freeMemory]);
      }
      fail("should get out of memory");
    } catch (OutOfMemoryError e) {}
  }
}
