package ca.uhn.fhir.validation;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.io.IOUtils;
import org.hl7.fhir.instance.model.Coding;
import org.hl7.fhir.instance.model.DataElement;
import org.hl7.fhir.instance.model.IntegerType;
import org.hl7.fhir.instance.model.Questionnaire;
import org.hl7.fhir.instance.model.Questionnaire.AnswerFormat;
import org.hl7.fhir.instance.model.Questionnaire.GroupComponent;
import org.hl7.fhir.instance.model.QuestionnaireResponse;
import org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionnaireResponseStatus;
import org.hl7.fhir.instance.model.Reference;
import org.hl7.fhir.instance.model.StringType;
import org.hl7.fhir.instance.model.ValueSet;
import org.hl7.fhir.instance.utils.WorkerContext;
import org.hl7.fhir.instance.validation.QuestionnaireResponseValidator;
import org.hl7.fhir.instance.validation.ValidationMessage;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.util.TestUtil;

public class QuestionnaireResponseValidatorTest {
  private static FhirContext ourCtx = FhirContext.forDstu2Hl7Org();

  private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(QuestionnaireResponseValidatorTest.class);
  private QuestionnaireResponseValidator myVal;

  private WorkerContext myWorkerCtx;

  @Before
  public void before() {
    myWorkerCtx = new WorkerContext();
    myVal = new QuestionnaireResponseValidator(myWorkerCtx);
  }

  @AfterClass
  public static void afterClassClearContext() {
    TestUtil.clearAllStaticFieldsForUnitTest();
  }

  @Test
  public void testAnswerWithWrongType() {
    Questionnaire q = new Questionnaire();
    q.getGroup().addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.BOOLEAN);

    QuestionnaireResponse qa = new QuestionnaireResponse();
    qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
    qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("FOO"));

    myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
    List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);

    ourLog.info(errors.toString());
    assertThat(errors.toString(), containsString("Answer to question with linkId[link0] found of type [StringType] but this is invalid for question of type [boolean]"));
  }

  @Test
  public void testCodedAnswer() {
    String questionnaireRef = "http://example.com/Questionnaire/q1";

    Questionnaire q = new Questionnaire();
    q.getGroup().addQuestion().setLinkId("link0").setRequired(false).setType(AnswerFormat.CHOICE).setOptions(new Reference("http://somevalueset"));
    myWorkerCtx.getQuestionnaires().put(questionnaireRef, q);

    ValueSet options = new ValueSet();
    options.getCodeSystem().setSystem("urn:system").addConcept().setCode("code0");
    options.getCompose().addInclude().setSystem("urn:system2").addConcept().setCode("code2");
    myWorkerCtx.getValueSets().put("http://somevalueset", options);

    QuestionnaireResponse qa;
    List<ValidationMessage> errors;

    // Good code

    qa = new QuestionnaireResponse();
    qa.getQuestionnaire().setReference(questionnaireRef);
    qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code0"));
    errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);
    assertEquals(errors.toString(), 0, errors.size());

    qa = new QuestionnaireResponse();
    qa.getQuestionnaire().setReference(questionnaireRef);
    qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system2").setCode("code2"));
    errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);
    assertEquals(errors.toString(), 0, errors.size());

    // Bad code

    qa = new QuestionnaireResponse();
    qa.getQuestionnaire().setReference(questionnaireRef);
    qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code1"));
    errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);
    ourLog.info(errors.toString());
    assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
    assertThat(errors.toString(),
        containsString("message=Question with linkId[link0] has answer with system[urn:system] and code[code1] but this is not a valid answer for ValueSet[http://somevalueset]"));

    qa = new QuestionnaireResponse();

    qa.getQuestionnaire().setReference(questionnaireRef);
    qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system2").setCode("code3"));
    errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);
    ourLog.info(errors.toString());
    assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
    assertThat(errors.toString(),
        containsString("message=Question with linkId[link0] has answer with system[urn:system2] and code[code3] but this is not a valid answer for ValueSet[http://somevalueset]"));

  }

  @Test
  public void testOpenchoiceAnswer() {
    String questionnaireRef = "http://example.com/Questionnaire/q1";

    Questionnaire q = new Questionnaire();
    q.getGroup().addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.OPENCHOICE).setOptions(new Reference("http://somevalueset"));
    myWorkerCtx.getQuestionnaires().put(questionnaireRef, q);

    ValueSet options = new ValueSet();
    options.getCodeSystem().setSystem("urn:system").addConcept().setCode("code0");
    options.getCompose().addInclude().setSystem("urn:system2").addConcept().setCode("code2");
    myWorkerCtx.getValueSets().put("http://somevalueset", options);

    QuestionnaireResponse qa;
    List<ValidationMessage> errors;

    // Good code

    qa = new QuestionnaireResponse();
    qa.getQuestionnaire().setReference(questionnaireRef);
    qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code0"));
    errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);
    assertEquals(errors.toString(), 0, errors.size());

    qa = new QuestionnaireResponse();
    qa.getQuestionnaire().setReference(questionnaireRef);
    qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system2").setCode("code2"));
    errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);
    assertEquals(errors.toString(), 0, errors.size());

    // Bad code

    qa = new QuestionnaireResponse();
    qa.getQuestionnaire().setReference(questionnaireRef);
    qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code1"));
    errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);
    ourLog.info(errors.toString());
    assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
    assertThat(errors.toString(),
        containsString("message=Question with linkId[link0] has answer with system[urn:system] and code[code1] but this is not a valid answer for ValueSet[http://somevalueset]"));

    // Partial code

    qa = new QuestionnaireResponse();
    qa.getQuestionnaire().setReference(questionnaireRef);
    qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem(null).setCode("code1"));
    errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);
    ourLog.info(errors.toString());
    assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
    assertThat(errors.toString(), containsString("message=Answer to question with linkId[link0] is of type OPENCHOICE but coding answer does not have a system"));

    qa = new QuestionnaireResponse();
    qa.getQuestionnaire().setReference(questionnaireRef);
    qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("").setCode("code1"));
    errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);
    ourLog.info(errors.toString());
    assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
    assertThat(errors.toString(), containsString("message=Answer to question with linkId[link0] is of type OPENCHOICE but coding answer does not have a system"));

    qa = new QuestionnaireResponse();
    qa.getQuestionnaire().setReference(questionnaireRef);
    qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("system").setCode(null));
    errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);
    ourLog.info(errors.toString());
    assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
    assertThat(errors.toString(), containsString("message=Answer to question with linkId[link0] is of type OPENCHOICE but coding answer does not have a code"));

    qa = new QuestionnaireResponse();
    qa.getQuestionnaire().setReference(questionnaireRef);
    qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("system").setCode(null));
    errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);
    ourLog.info(errors.toString());
    assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
    assertThat(errors.toString(), containsString("message=Answer to question with linkId[link0] is of type OPENCHOICE but coding answer does not have a code"));

    // Wrong type

    qa = new QuestionnaireResponse();
    qa.getQuestionnaire().setReference(questionnaireRef);
    qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new IntegerType(123));
    errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);
    ourLog.info(errors.toString());
    assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
    assertThat(errors.toString(), containsString("message=Answer to question with linkId[link0] found of type [IntegerType] but this is invalid for question of type [open-choice]"));

    // String answer

    qa = new QuestionnaireResponse();
    qa.getQuestionnaire().setReference(questionnaireRef);
    qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("Hello"));
    errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);
    ourLog.info(errors.toString());
    assertThat(errors, empty());

    // Missing String answer

    qa = new QuestionnaireResponse();
    qa.getQuestionnaire().setReference(questionnaireRef);
    qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new StringType(""));
    errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);
    ourLog.info(errors.toString());
    assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
    assertThat(errors.toString(), containsString("Answer to question with linkId[link0] is required but answer does not have a value"));

  }

  @Test
  public void testExtensionDereference() throws Exception {
    Questionnaire q = ourCtx.newJsonParser().parseResource(Questionnaire.class, IOUtils.toString(getClass().getResourceAsStream("/dereference-q.json")));
    QuestionnaireResponse qa = ourCtx.newXmlParser().parseResource(QuestionnaireResponse.class, IOUtils.toString(getClass().getResourceAsStream("/dereference-qr.xml")));
    DataElement de = ourCtx.newJsonParser().parseResource(DataElement.class, IOUtils.toString(getClass().getResourceAsStream("/dereference-de.json")));

    myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
    myWorkerCtx.getDataElements().put("DataElement/4771", de);
    List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);

    ourLog.info(errors.toString());
    assertEquals(errors.toString(), errors.size(), 0);
  }

  @Test
  public void testGroupWithNoLinkIdInQuestionnaireResponse() {
    Questionnaire q = new Questionnaire();
    GroupComponent qGroup = q.getGroup().addGroup();
    qGroup.addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.BOOLEAN);

    QuestionnaireResponse qa = new QuestionnaireResponse();
    qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
    org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent qaGroup = qa.getGroup().addGroup();
    qaGroup.addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("FOO"));

    myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
    List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);

    ourLog.info(errors.toString());
    assertThat(errors.toString(), containsString("Answer to question with linkId[link0] found of type [StringType] but this is invalid for question of type [boolean]"));
  }

  @Test
  public void testMissingRequiredQuestion() {

    Questionnaire q = new Questionnaire();
    q.getGroup().addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.STRING);
    q.getGroup().addQuestion().setLinkId("link1").setRequired(true).setType(AnswerFormat.STRING);

    QuestionnaireResponse qa = new QuestionnaireResponse();
    qa.setStatus(QuestionnaireResponseStatus.COMPLETED);
    qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
    qa.getGroup().addQuestion().setLinkId("link1").addAnswer().setValue(new StringType("FOO"));

    myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
    List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);

    ourLog.info(errors.toString());
    assertThat(errors.toString(), containsString("Missing answer to required question with linkId[link0]"));
  }

  @Test
  public void testMultipleGroupsWithNoLinkIdInQuestionnaire() {
    Questionnaire q = new Questionnaire();
    GroupComponent qGroup = q.getGroup().addGroup();
    qGroup.addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.BOOLEAN);
    qGroup = q.getGroup().addGroup();
    qGroup.addQuestion().setLinkId("link1").setRequired(true).setType(AnswerFormat.BOOLEAN);

    QuestionnaireResponse qa = new QuestionnaireResponse();
    qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
    org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent qaGroup = qa.getGroup().addGroup();
    qaGroup.addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("FOO"));

    myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
    List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);

    ourLog.info(errors.toString());
    assertThat(errors.toString(), containsString(
        "ValidationMessage[level=FATAL,type=BUSINESSRULE,location=//QuestionnaireResponse/group[0],message=Questionnaire in invalid, unable to validate QuestionnaireResponse: Multiple groups found at this position with blank/missing linkId]"));
    assertEquals(1, errors.size());
  }

  @Test
  public void testUnexpectedAnswer() {
    Questionnaire q = new Questionnaire();
    q.getGroup().addQuestion().setLinkId("link0").setRequired(false).setType(AnswerFormat.BOOLEAN);

    QuestionnaireResponse qa = new QuestionnaireResponse();
    qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
    qa.getGroup().addQuestion().setLinkId("link1").addAnswer().setValue(new StringType("FOO"));

    myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
    List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);

    ourLog.info(errors.toString());
    assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]"));
    assertThat(errors.toString(), containsString("message=Found answer with linkId[link1] but this ID is not allowed at this position"));
  }

  @Test
  public void testUnexpectedGroup() {
    Questionnaire q = new Questionnaire();
    q.getGroup().addQuestion().setLinkId("link0").setRequired(false).setType(AnswerFormat.BOOLEAN);

    QuestionnaireResponse qa = new QuestionnaireResponse();
    qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
    qa.getGroup().addGroup().setLinkId("link1");

    myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
    List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);

    ourLog.info(errors.toString());
    assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/group[0]"));
    assertThat(errors.toString(), containsString("Group with linkId[link1] found at this position, but this group does not exist at this position in Questionnaire"));
  }

  // @Test
  public void validateHealthConnexExample() throws Exception {
    String input = IOUtils.toString(QuestionnaireResponseValidatorTest.class.getResourceAsStream("/questionnaireanswers-0f431c50ddbe4fff8e0dd6b7323625fc.xml"));

    QuestionnaireResponse qa = ourCtx.newXmlParser().parseResource(QuestionnaireResponse.class, input);
    ArrayList<ValidationMessage> errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);
    assertEquals(errors.toString(), 0, errors.size());

    /*
     * Now change a coded value
     */
    // @formatter:off
    input = input.replaceAll(
        "<answer>\n" + "					<valueCoding>\n" + "						<system value=\"f69573b8-cb63-4d31-85a4-23ac784735ab\"/>\n" + "						<code value=\"2\"/>\n"
            + "						<display value=\"Once/twice\"/>\n" + "					</valueCoding>\n" + "				</answer>",
        "<answer>\n" + "					<valueCoding>\n" + "						<system value=\"f69573b8-cb63-4d31-85a4-23ac784735ab\"/>\n" + "						<code value=\"GGG\"/>\n"
            + "						<display value=\"Once/twice\"/>\n" + "					</valueCoding>\n" + "				</answer>");
    assertThat(input, containsString("GGG"));
    // @formatter:on

    qa = ourCtx.newXmlParser().parseResource(QuestionnaireResponse.class, input);
    errors = new ArrayList<ValidationMessage>();
    myVal.validate(errors, qa);
    assertEquals(errors.toString(), 10, errors.size());
  }

}
