package org.mitre.synthea.engine;

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

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.stream.JsonReader;

import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.LinkedList;
import java.util.concurrent.ConcurrentHashMap;

import org.junit.Before;
import org.junit.Test;
import org.mitre.synthea.TestHelper;
import org.mitre.synthea.helpers.Config;
import org.mitre.synthea.helpers.Utilities;
import org.mitre.synthea.world.agents.PayerManager;
import org.mitre.synthea.world.agents.Person;
import org.mitre.synthea.world.agents.Provider;
import org.mitre.synthea.world.concepts.HealthRecord;
import org.mitre.synthea.world.concepts.HealthRecord.CarePlan;
import org.mitre.synthea.world.concepts.HealthRecord.EncounterType;
import org.mitre.synthea.world.concepts.HealthRecord.Observation;
import org.mitre.synthea.world.concepts.VitalSign;
import org.mitre.synthea.world.geography.Location;
import org.mockito.Mockito;

public class LogicTest {
  private Person person;
  private long time;

  private JsonObject tests;

  /**
   * Setup logic tests.
   * @throws IOException On File IO errors.
   */
  @Before
  public void setup() throws IOException {
    Config.set("exporter.split_records", "false");
    person = new Person(0L);
    // Give person an income to prevent null pointer.
    person.attributes.put(Person.INCOME, 10000000);
    Provider mock = Mockito.mock(Provider.class);
    Mockito.when(mock.getResourceID()).thenReturn("Mock-Provider");
    for (EncounterType type : EncounterType.values()) {
      person.setProvider(type, mock);
    }

    mock = Mockito.mock(Provider.class);
    Mockito.when(mock.getResourceID()).thenReturn("Mock-Emergency");
    person.setProvider(EncounterType.EMERGENCY, mock);
    long birthTime = 0L;
    person.attributes.put(Person.BIRTHDATE, birthTime);
    time = System.currentTimeMillis();
    // Ensure Person's Payer is not null.
    String testStateDefault = Config.get("test_state.default", "Massachusetts");
    PayerManager.loadPayers(new Location(testStateDefault, null));
    person.coverage.setPlanToNoInsurance((long) person.attributes.get(Person.BIRTHDATE));
    for (int i = 1; i <= Utilities.getYear(time - birthTime); i++) {
      person.coverage.newEnrollmentPeriod(birthTime + Utilities.convertTime("years", i));
    }
    person.coverage.setPlanToNoInsurance(time);

    Path modulesFolder = Paths.get("src/test/resources/generic");
    Path logicFile = modulesFolder.resolve("logic.json");
    JsonReader reader = new JsonReader(new FileReader(logicFile.toString()));
    tests = JsonParser.parseReader(reader).getAsJsonObject();
    reader.close();
  }

  private boolean doTest(String testName) {
    JsonObject definition = tests.getAsJsonObject(testName).deepCopy();
    Logic logic = Utilities.getGson().fromJson(definition, Logic.class);

    return logic.test(person, time);
  }

  @Test
  public void testTrue() {
    assertTrue(doTest("trueTest"));
  }

  @Test
  public void testFalse() {
    assertFalse(doTest("falseTest"));
  }

  @Test
  public void testGenderCondition() {
    person.attributes.put(Person.GENDER, "M");
    assertTrue(doTest("genderIsMaleTest"));

    person.attributes.put(Person.GENDER, "F");
    assertFalse(doTest("genderIsMaleTest"));
  }

  private void setPatientAge(int age) {
    LocalDateTime now = LocalDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneId.of("UTC"));

    LocalDateTime bday = now.minus(age, ChronoUnit.YEARS);
    long birthdate = bday.toInstant(ZoneOffset.UTC).toEpochMilli();
    person.attributes.put(Person.BIRTHDATE, birthdate);
    person.attributes.remove(Person.BIRTHDATE_AS_LOCALDATE);
  }

  @Test
  public void testAgeConditionsOnAge35() {
    setPatientAge(35);
    assertTrue(doTest("ageLt40Test"));
    assertTrue(doTest("ageLte40Test"));
    assertFalse(doTest("ageEq40Test"));
    assertFalse(doTest("ageGte40Test"));
    assertFalse(doTest("ageGt40Test"));
    assertTrue(doTest("ageNe40Test"));
  }

  @Test
  public void testAgeConditionsOnAge40() {
    setPatientAge(40);
    assertFalse(doTest("ageLt40Test"));
    assertTrue(doTest("ageLte40Test"));
    assertTrue(doTest("ageEq40Test"));
    assertTrue(doTest("ageGte40Test"));
    assertFalse(doTest("ageGt40Test"));
    assertFalse(doTest("ageNe40Test"));
  }

  @Test
  public void testAgeConditionsOnAge45() {
    setPatientAge(45);
    assertFalse(doTest("ageLt40Test"));
    assertFalse(doTest("ageLte40Test"));
    assertFalse(doTest("ageEq40Test"));
    assertTrue(doTest("ageGte40Test"));
    assertTrue(doTest("ageGt40Test"));
    assertTrue(doTest("ageNe40Test"));
  }

  @Test
  public void test_race_exists() {
    person.attributes.put(Person.RACE, "white");
    assertTrue(doTest("raceExistsTest"));

    person.attributes.put(Person.RACE, "native");
    assertFalse(doTest("raceDoesNotExistTest"));
  }

  @Test
  public void test_date() {
    time = TestHelper.timestamp(2016, 9, 21, 0, 0, 0);
    assertFalse(doTest("before2016Test"));
    assertTrue(doTest("after2000Test"));

    time = TestHelper.timestamp(1981, 4, 28, 0, 0, 0);
    assertTrue(doTest("before2016Test"));
    assertFalse(doTest("after2000Test"));

    time = TestHelper.timestamp(2002, 2, 22, 0, 0, 0);
    assertTrue(doTest("before2016Test"));
    assertTrue(doTest("after2000Test"));

    time = TestHelper.timestamp(2000, 12, 10, 0, 0, 0);
    assertFalse(doTest("beforeSeptemberTest"));
    assertTrue(doTest("afterAprilTest"));
    assertFalse(doTest("inJulyTest"));

    time = TestHelper.timestamp(2004, 2, 8, 0, 0, 0);
    assertTrue(doTest("beforeSeptemberTest"));
    assertFalse(doTest("afterAprilTest"));
    assertFalse(doTest("inJulyTest"));

    time = TestHelper.timestamp(2012, 7, 17, 0, 0, 0);
    assertTrue(doTest("beforeSeptemberTest"));
    assertTrue(doTest("afterAprilTest"));
    assertTrue(doTest("inJulyTest"));

    time = TestHelper.timestamp(2016, 12, 30, 0, 0, 0);
    assertFalse(doTest("beforeChristmas2016Test"));
    assertTrue(doTest("afterIndependenceDay2000Test"));
    assertFalse(doTest("isHalloween2007Test"));

    time = TestHelper.timestamp(2000, 4, 4, 0, 0, 0);
    assertTrue(doTest("beforeChristmas2016Test"));
    assertFalse(doTest("afterIndependenceDay2000Test"));
    assertFalse(doTest("isHalloween2007Test"));

    time = TestHelper.timestamp(2007, 10, 31, 0, 0, 0);
    assertTrue(doTest("beforeChristmas2016Test"));
    assertTrue(doTest("afterIndependenceDay2000Test"));
    assertTrue(doTest("isHalloween2007Test"));
  }

  @Test
  public void test_attribute() {
    String attribute = "Test_Attribute_Key";

    person.attributes.remove(attribute);
    assertFalse(doTest("attributeEqualTo_TestValue_Test"));
    assertFalse(doTest("attributeLt_String_Test"));
    assertTrue(doTest("attributeNilTest"));
    assertFalse(doTest("attributeNotNilTest"));

    person.attributes.put(attribute, "Wrong Value");
    assertFalse(doTest("attributeEqualTo_TestValue_Test"));
    assertFalse(doTest("attributeLt_String_Test"));
    assertFalse(doTest("attributeNilTest"));
    assertTrue(doTest("attributeNotNilTest"));

    person.attributes.put(attribute, "TestValue");
    assertTrue(doTest("attributeEqualTo_TestValue_Test"));
    assertTrue(doTest("attributeLt_String_Test"));
    assertFalse(doTest("attributeNilTest"));
    assertTrue(doTest("attributeNotNilTest"));

    person.attributes.put(attribute, 120);
    assertTrue(doTest("attributeGt100Test"));
    assertFalse(doTest("attributeNilTest"));
    assertTrue(doTest("attributeNotNilTest"));
  }

  @Test(expected = RuntimeException.class)
  public void test_attribute_expected_numeric_exception() {
    String attribute = "Test_Attribute_Key";
    person.attributes.put(attribute, "TestValue");
    doTest("attributeGt100Test");
  }

  @Test(expected = RuntimeException.class)
  public void test_attribute_expected_string_exception() {
    String attribute = "Test_Attribute_Key";
    person.attributes.put(attribute, 120);
    doTest("attributeEqualTo_TestValue_Test");
  }

  @Test
  public void test_symptoms() {
    person.setSymptom(
        "Module1", "Appendicitis", "PainLevel", System.currentTimeMillis(), 60, false
    );
    assertTrue(doTest("symptomPainLevelGt50"));
    assertTrue(doTest("symptomPainLevelLte80"));

    // painlevel still 60 here
    person.setSymptom(
        "Module2", "Appendicitis", "LackOfAppetite", System.currentTimeMillis(), 100, false
    );
    assertTrue(doTest("symptomPainLevelGt50"));
    assertTrue(doTest("symptomPainLevelLte80"));

    person.setSymptom(
        "Module1", "Appendicitis", "PainLevel", System.currentTimeMillis(), 10, false);
    assertFalse(doTest("symptomPainLevelGt50"));
    assertTrue(doTest("symptomPainLevelLte80"));

    person.setSymptom(
        "Module3", "Appicitis", "PainLevel", System.currentTimeMillis(), 100, false);
    assertTrue(doTest("symptomPainLevelGt50"));
    assertFalse(doTest("symptomPainLevelLte80"));
  }

  @Test
  public void test_vital_signs() {
    person.setVitalSign(VitalSign.SYSTOLIC_BLOOD_PRESSURE, 100);
    assertFalse(doTest("SystolicBloodPressureGt120"));

    person.setVitalSign(VitalSign.SYSTOLIC_BLOOD_PRESSURE, 140);
    assertTrue(doTest("SystolicBloodPressureGt120"));
  }

  @Test(expected = NullPointerException.class)
  public void test_vital_sign_missing() {
    // starts out vital signs clear
    doTest("SystolicBloodPressureGt120");
  }

  @Test(expected = NullPointerException.class)
  public void test_missing_observation() {
    // starts out with no observations
    doTest("mmseObservationGt22");
  }

  @Test
  public void test_logic_with_split_record_no_duplicates() throws Exception {
    Module module = TestHelper.getFixture("switching_provider.json");
    Config.set("exporter.split_records.duplicate_data", "false");
    person.hasMultipleRecords = true;
    person.records = new ConcurrentHashMap<String, HealthRecord>();
    module.process(person, time);
    long nextStep = time + Utilities.convertTime("days", 7);
    module.process(person, nextStep);
    assertTrue(person.hasMultipleRecords);
    assertEquals(2, person.records.size());
    assertEquals(0, person.record.currentEncounter(time).conditions.size());
    assertEquals(0, person.record.currentEncounter(time).careplans.size());
    assertEquals(0, person.record.currentEncounter(time).medications.size());
    assertEquals(0, person.record.currentEncounter(time).observations.size());
    assertTrue((Boolean) person.attributes.getOrDefault("found_condition", false));
    assertTrue((Boolean) person.attributes.getOrDefault("found_careplan", false));
    assertTrue((Boolean) person.attributes.getOrDefault("found_medication", false));
    person.hasMultipleRecords = false;
    person.records = null;
  }

  @Test
  public void test_logic_with_split_record_with_duplicates() throws Exception {
    Module module = TestHelper.getFixture("switching_provider.json");
    Config.set("exporter.split_records.duplicate_data", "true");
    person.hasMultipleRecords = true;
    person.records = new ConcurrentHashMap<String, HealthRecord>();
    module.process(person, time);
    long nextStep = time + Utilities.convertTime("days", 7);
    module.process(person, nextStep);
    person.record = person.records.get("Mock-Provider");
    assertTrue(person.hasMultipleRecords);
    assertEquals(2, person.records.size());
    assertEquals(1, person.record.currentEncounter(time).conditions.size());
    assertEquals(1, person.record.currentEncounter(time).careplans.size());
    assertEquals(1, person.record.currentEncounter(time).medications.size());
    assertEquals(1, person.record.currentEncounter(time).observations.size());
    assertTrue((Boolean) person.attributes.getOrDefault("found_condition", false));
    assertTrue((Boolean) person.attributes.getOrDefault("found_careplan", false));
    assertTrue((Boolean) person.attributes.getOrDefault("found_medication", false));
    person.hasMultipleRecords = false;
    person.records = null;
  }

  private void clearRecord(Person person) {
    person.initializeDefaultHealthRecords();
    person.releaseCurrentEncounter(0L, null);
  }

  @Test
  public void test_observations() {

    HealthRecord.Code mmseCode = new HealthRecord.Code("LOINC", "72107-6",
        "Mini Mental State Examination");

    Observation obs = person.record.observation(time, mmseCode.code, 12);
    obs.codes.add(mmseCode);
    assertFalse(doTest("mmseObservationGt22"));

    clearRecord(person);

    obs = person.record.observation(time, mmseCode.code, 29);
    obs.codes.add(mmseCode);
    assertTrue(doTest("mmseObservationGt22"));

    clearRecord(person);

    HealthRecord.Code valueCodeFalse = new HealthRecord.Code("LOINC", "72107-8",
        "Other Observation Value");

    obs = person.record.observation(time, mmseCode.code, valueCodeFalse);
    obs.codes.add(mmseCode);
    assertFalse(doTest("ObservationEqValueCode"));

    clearRecord(person);

    HealthRecord.Code valueCodeTrue = new HealthRecord.Code("LOINC", "72107-7",
        "Some Observation Value");

    obs = person.record.observation(time, mmseCode.code, valueCodeTrue);
    obs.codes.add(mmseCode);
    assertTrue(doTest("ObservationEqValueCode"));

    clearRecord(person);
    assertFalse(doTest("hasDiabetesObservation"));

    obs = person.record.observation(time, "Blood Panel", "blah blah");
    person.attributes.put("Blood Test Performed", obs);
    assertFalse(doTest("hasDiabetesObservation"));

    obs = person.record.observation(time, "Glucose Panel", "12345");
    person.attributes.put("Diabetes Test Performed", obs);
    assertTrue(doTest("hasDiabetesObservation"));
  }

  @Test
  public void test_condition_condition() {
    clearRecord(person);
    assertFalse(doTest("diabetesConditionTest"));
    assertFalse(doTest("alzheimersConditionTest"));

    HealthRecord.Code diabetesCode = new HealthRecord.Code("SNOMED-CT", "73211009",
        "Diabetes mellitus");

    person.record.conditionStart(time, diabetesCode.code);
    assertTrue(doTest("diabetesConditionTest"));
    assertFalse(doTest("alzheimersConditionTest"));

    time += Utilities.convertTime("years", 10);

    person.record.conditionEnd(time, diabetesCode.code);
    assertFalse(doTest("diabetesConditionTest"));

    HealthRecord.Code alzCode = new HealthRecord.Code("SNOMED-CT", "26929004",
        "Alzheimer's disease (disorder)");

    HealthRecord.Entry cond = person.record.conditionStart(time, alzCode.code);
    person.attributes.put("Alzheimer's Variant", cond);

    assertTrue(doTest("alzheimersConditionTest"));
  }

  @Test
  public void test_allergy_condition() {
    clearRecord(person);
    assertFalse(doTest("penicillinAllergyTest"));

    HealthRecord.Code penicillinCode = new HealthRecord.Code("RxNorm", "7984",
        "Penicillin V");

    person.record.allergyStart(time, penicillinCode.code);
    assertTrue(doTest("penicillinAllergyTest"));

    time += Utilities.convertTime("years", 10);

    person.record.allergyEnd(time, penicillinCode.code);
    assertFalse(doTest("penicillinAllergyTest"));
  }

  @Test
  public void test_careplan_condition() {

    HealthRecord.Code diabetesCode = new HealthRecord.Code("SNOMED-CT", "698360004",
        "Diabetes self management plan");

    clearRecord(person);
    assertFalse(doTest("diabetesCarePlanTest"));
    assertFalse(doTest("anginaCarePlanTest"));

    CarePlan dcp = person.record.careplanStart(time, diabetesCode.code);
    dcp.codes.add(diabetesCode);
    assertTrue(doTest("diabetesCarePlanTest"));
    assertFalse(doTest("anginaCarePlanTest"));

    time += Utilities.convertTime("years", 10);

    person.record.careplanEnd(time, diabetesCode.code, new HealthRecord.Code("SNOMED-CT",
        "444110003", "Type II Diabetes Mellitus Well Controlled"));
    assertFalse(doTest("diabetesCarePlanTest"));

    HealthRecord.Code anginaCode = new HealthRecord.Code("SNOMED-CT", "698360004",
        "Diabetes self management plan");

    CarePlan acp = person.record.careplanStart(time, anginaCode.code);
    acp.codes.add(anginaCode);
    person.attributes.put("Angina_CarePlan", acp);
    assertTrue(doTest("anginaCarePlanTest"));
  }

  @Test
  public void test_ses_category() {
    person.attributes.put(Person.SOCIOECONOMIC_CATEGORY, "High");
    assertTrue(doTest("sesHighTest"));
    assertFalse(doTest("sesMiddleTest"));
    assertFalse(doTest("sesLowTest"));

    person.attributes.put(Person.SOCIOECONOMIC_CATEGORY, "Middle");
    assertFalse(doTest("sesHighTest"));
    assertTrue(doTest("sesMiddleTest"));
    assertFalse(doTest("sesLowTest"));

    person.attributes.put(Person.SOCIOECONOMIC_CATEGORY, "Low");
    assertFalse(doTest("sesHighTest"));
    assertFalse(doTest("sesMiddleTest"));
    assertTrue(doTest("sesLowTest"));
  }

  @Test
  public void test_prior_state() {
    person.history = new LinkedList<>();
    assertFalse(doTest("priorStateDoctorVisitTest"));
    assertFalse(doTest("priorStateCarePlanSinceDoctorVisitTest"));
    assertFalse(doTest("priorStateDoctorVisitWithin3YearsTest"));
    assertFalse(doTest("priorStateCarePlanSinceDoctorVisitWithin3YearsTest"));

    State state = new State.Simple();
    state.name = "CarePlan";
    state.entered = state.exited = time;
    person.history.add(0, state);
    assertFalse(doTest("priorStateDoctorVisitTest"));
    assertTrue(doTest("priorStateCarePlanSinceDoctorVisitTest"));
    assertFalse(doTest("priorStateDoctorVisitWithin3YearsTest"));
    assertTrue(doTest("priorStateCarePlanSinceDoctorVisitWithin3YearsTest"));

    state = new State.Simple();
    state.name = "DoctorVisit";
    state.entered = state.exited = time;
    person.history.add(0, state);
    assertTrue(doTest("priorStateDoctorVisitTest"));
    assertFalse(doTest("priorStateCarePlanSinceDoctorVisitTest"));
    assertTrue(doTest("priorStateDoctorVisitWithin3YearsTest"));
    assertFalse(doTest("priorStateCarePlanSinceDoctorVisitWithin3YearsTest"));

    time += Utilities.convertTime("years", 2);

    state = new State.Simple();
    state.name = "CarePlan";
    state.entered = state.exited = time;
    person.history.add(0, state);
    assertTrue(doTest("priorStateDoctorVisitTest"));
    assertTrue(doTest("priorStateCarePlanSinceDoctorVisitTest"));
    assertTrue(doTest("priorStateDoctorVisitWithin3YearsTest"));
    assertTrue(doTest("priorStateCarePlanSinceDoctorVisitWithin3YearsTest"));

    time += Utilities.convertTime("years", 5);

    assertTrue(doTest("priorStateDoctorVisitTest"));
    assertTrue(doTest("priorStateCarePlanSinceDoctorVisitTest"));
    assertFalse(doTest("priorStateDoctorVisitWithin3YearsTest"));
    assertFalse(doTest("priorStateCarePlanSinceDoctorVisitWithin3YearsTest"));
  }

  @Test
  public void test_and_conditions() {
    assertTrue(doTest("andAllTrueTest"));
    assertFalse(doTest("andOneFalseTest"));
    assertFalse(doTest("andAllFalseTest"));
  }

  @Test
  public void test_or_conditions() {
    assertTrue(doTest("orAllTrueTest"));
    assertTrue(doTest("orOneTrueTest"));
    assertFalse(doTest("orAllFalseTest"));
  }

  @Test
  public void test_at_least_condition() {
    assertTrue(doTest("atLeast3_AllTrueTest"));
    assertTrue(doTest("atLeast3_3TrueTest"));
    assertFalse(doTest("atLeast3_2TrueTest"));
    assertFalse(doTest("atLeast3_NoneTrueTest"));
  }

  @Test
  public void test_at_most_condition() {
    assertFalse(doTest("atMost2_AllTrueTest"));
    assertFalse(doTest("atMost2_3TrueTest"));
    assertTrue(doTest("atMost2_2TrueTest"));
    assertTrue(doTest("atMost2_NoneTrueTest"));
  }

  @Test
  public void test_not_conditions() {
    assertFalse(doTest("notTrueTest"));
    assertTrue(doTest("notFalseTest"));
  }
}
