package ca.uhn.fhir.rest.server;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.BundleEntry;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt;
import ca.uhn.fhir.model.dstu.resource.*;
import ca.uhn.fhir.model.dstu.resource.Conformance.RestResource;
import ca.uhn.fhir.model.dstu.resource.Conformance.RestResourceSearchParam;
import ca.uhn.fhir.model.dstu.valueset.ResourceTypeEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.BeforeEachClass;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.*;

/**
 * Created by dsotnikov on 2/25/2014.
 */
public class ReferenceParameterTest {

	private static CloseableHttpClient ourClient;
	private static FhirContext ourCtx = FhirContext.forDstu1();
	private static ReferenceParam ourLastRefParam;
	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ReferenceParameterTest.class);
	private static int ourPort;
	private static Server ourServer;

	@BeforeEach
	public void before() {
		ourLastRefParam = null;
	}

	@Test
	public void testParamTypesInConformanceStatement() throws Exception {
		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/metadata?_pretty=true");
		HttpResponse status = ourClient.execute(httpGet);
		String responseContent = IOUtils.toString(status.getEntity().getContent());
		IOUtils.closeQuietly(status.getEntity().getContent());

		ourLog.info(responseContent);

		assertEquals(200, status.getStatusLine().getStatusCode());
		Conformance conf = ourCtx.newXmlParser().parseResource(Conformance.class, responseContent);

		RestResource res = conf.getRestFirstRep().getResource().get(2);
		assertEquals("Patient", res.getType().getValue());

		RestResourceSearchParam param = res.getSearchParamFirstRep();
		assertEquals(Patient.SP_PROVIDER, param.getName().getValue());

		assertEquals(1, param.getTarget().size());
		assertEquals(ResourceTypeEnum.ORGANIZATION, param.getTarget().get(0).getValueAsEnum());
	}

	@Test
	public void testReadReturnVersionedReferenceInResponse() throws Exception {
		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/22");
		HttpResponse status = ourClient.execute(httpGet);
		String responseContent = IOUtils.toString(status.getEntity().getContent());
		IOUtils.closeQuietly(status.getEntity().getContent());

		assertEquals(200, status.getStatusLine().getStatusCode());
		Patient p = ourCtx.newXmlParser().parseResource(Patient.class, responseContent);

		assertThat(status.getFirstHeader("Content-Location").getValue(), containsString("Patient/22/_history/33"));

		assertEquals("44", p.getManagingOrganization().getReference().getIdPart());
		assertEquals(null, p.getManagingOrganization().getReference().getVersionIdPart());
	}

	@Test
	public void testReferenceParamViewToken() throws Exception {
		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?provider.name=" + UrlUtil.escape("foo|bar"));
		HttpResponse status = ourClient.execute(httpGet);
		IOUtils.toString(status.getEntity().getContent());
		IOUtils.closeQuietly(status.getEntity().getContent());

		assertEquals(200, status.getStatusLine().getStatusCode());
		assertEquals("foo|bar", ourLastRefParam.getValue());
		assertEquals("foo", ourLastRefParam.toTokenParam(ourCtx).getSystem());
		assertEquals("bar", ourLastRefParam.toTokenParam(ourCtx).getValue());
	}

	@Test
	public void testSearchReturnVersionedReferenceInResponse() throws Exception {
		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_query=findPatientWithVersion");
		HttpResponse status = ourClient.execute(httpGet);
		String responseContent = IOUtils.toString(status.getEntity().getContent());
		IOUtils.closeQuietly(status.getEntity().getContent());

		assertEquals(200, status.getStatusLine().getStatusCode());
		List<BundleEntry> entries = ourCtx.newXmlParser().parseBundle(responseContent).getEntries();
		assertEquals(2, entries.size());
		Patient p = (Patient) entries.get(0).getResource();

		assertEquals(0, p.getContained().getContainedResources().size());
		assertEquals("22", p.getId().getIdPart());
		assertEquals("33", p.getId().getVersionIdPart());

		assertEquals("44", p.getManagingOrganization().getReference().getIdPart());
		assertEquals(null, p.getManagingOrganization().getReference().getVersionIdPart());
	}

	@Test
	public void testSearchWithMultipleParamsOfTheSameName1() throws Exception {
		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Organization?partof=po123");
		HttpResponse status = ourClient.execute(httpGet);
		String responseContent = IOUtils.toString(status.getEntity().getContent());
		IOUtils.closeQuietly(status.getEntity().getContent());
		assertEquals(200, status.getStatusLine().getStatusCode());
		assertThat(responseContent, containsString("value=\"thePartOfId po123 null\""));
	}

	@Test
	public void testSearchWithMultipleParamsOfTheSameName2() throws Exception {
		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Organization?partof.name=poname");
		HttpResponse status = ourClient.execute(httpGet);
		String responseContent = IOUtils.toString(status.getEntity().getContent());
		IOUtils.closeQuietly(status.getEntity().getContent());
		assertEquals(200, status.getStatusLine().getStatusCode());
		assertThat(responseContent, containsString("value=\"thePartOfName poname\""));
	}

	@Test
	public void testSearchWithMultipleParamsOfTheSameName3() throws Exception {
		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Organization?partof=po123&partof.name=poname");
		HttpResponse status = ourClient.execute(httpGet);
		String responseContent = IOUtils.toString(status.getEntity().getContent());
		IOUtils.closeQuietly(status.getEntity().getContent());
		assertEquals(200, status.getStatusLine().getStatusCode());
		assertThat(responseContent, containsString("value=\"thePartOfId po123 null\""));
		assertThat(responseContent, containsString("value=\"thePartOfName poname\""));
	}

	@Test
	public void testSearchWithMultipleParamsOfTheSameName4() throws Exception {
		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Organization?partof.fooChain=po123&partof.name=poname");
		HttpResponse status = ourClient.execute(httpGet);
		String responseContent = IOUtils.toString(status.getEntity().getContent());
		IOUtils.closeQuietly(status.getEntity().getContent());
		assertEquals(200, status.getStatusLine().getStatusCode());
		assertThat(responseContent, containsString("value=\"thePartOfId po123 fooChain\""));
		assertThat(responseContent, containsString("value=\"thePartOfName poname\""));
	}

	@Test
	public void testSearchWithMultipleParamsOfTheSameName5() throws Exception {
		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Organization?partof.bar=po123");
		HttpResponse status = ourClient.execute(httpGet);
		String responseContent = IOUtils.toString(status.getEntity().getContent());
		IOUtils.closeQuietly(status.getEntity().getContent());
		assertEquals(200, status.getStatusLine().getStatusCode());
		assertThat(responseContent, containsString("value=\"theBarId po123 bar\""));
	}

	@Test
	public void testSearchWithMultipleParamsOfTheSameName6() throws Exception {
		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Organization?partof:Organization.bar=po123");
		HttpResponse status = ourClient.execute(httpGet);
		String responseContent = IOUtils.toString(status.getEntity().getContent());
		IOUtils.closeQuietly(status.getEntity().getContent());
		assertEquals(200, status.getStatusLine().getStatusCode());
		assertThat(responseContent, containsString("value=\"theBarId Organization/po123 bar\""));
	}

	@Test
	public void testSearchWithMultipleParamsOfTheSameName7() throws Exception {
		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Organization?partof:Organization=po123");
		HttpResponse status = ourClient.execute(httpGet);
		String responseContent = IOUtils.toString(status.getEntity().getContent());
		IOUtils.closeQuietly(status.getEntity().getContent());
		assertEquals(200, status.getStatusLine().getStatusCode());
		assertThat(responseContent, containsString("value=\"thePartOfId Organization/po123 null\""));
	}

	@Test
	public void testSearchWithMultipleParamsOfTheSameName8() throws Exception {
		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Location?partof=po123");
		HttpResponse status = ourClient.execute(httpGet);
		IOUtils.closeQuietly(status.getEntity().getContent());
		assertEquals(400, status.getStatusLine().getStatusCode());
	}

	@Test
	public void testSearchWithValue() throws Exception {
		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?" + Patient.SP_PROVIDER + "=123");
		HttpResponse status = ourClient.execute(httpGet);
		String responseContent = IOUtils.toString(status.getEntity().getContent());
		IOUtils.closeQuietly(status.getEntity().getContent());
		IOUtils.closeQuietly(status.getEntity().getContent());

		assertEquals(200, status.getStatusLine().getStatusCode());
		List<BundleEntry> entries = ourCtx.newXmlParser().parseBundle(responseContent).getEntries();
		assertEquals(1, entries.size());
		Patient p = (Patient) entries.get(0).getResource();
		assertEquals("0123", p.getName().get(0).getFamilyFirstRep().getValue());
		assertEquals("1", p.getName().get(1).getFamilyFirstRep().getValue());
		assertEquals("2", p.getName().get(2).getFamilyFirstRep().getValue());
	}

	
	
	@Test
	public void testSearchWithValueAndChain() throws Exception {
		{
			HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?" + Patient.SP_PROVIDER + ".name=123");
			HttpResponse status = ourClient.execute(httpGet);
			String responseContent = IOUtils.toString(status.getEntity().getContent());
			IOUtils.closeQuietly(status.getEntity().getContent());

			assertEquals(200, status.getStatusLine().getStatusCode());
			List<BundleEntry> entries = ourCtx.newXmlParser().parseBundle(responseContent).getEntries();
			assertEquals(1, entries.size());
			Patient p = (Patient) entries.get(0).getResource();
			assertEquals("0123", p.getName().get(0).getFamilyFirstRep().getValue());
			assertEquals("1", p.getName().get(1).getFamilyFirstRep().getValue());
			assertEquals("2name", p.getName().get(2).getFamilyFirstRep().getValue());
		}
	}

	@Test
	public void testSearchWithValueAndType() throws Exception {
		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?" + Patient.SP_PROVIDER + ":Organization=123");
		HttpResponse status = ourClient.execute(httpGet);
		String responseContent = IOUtils.toString(status.getEntity().getContent());
		IOUtils.closeQuietly(status.getEntity().getContent());

		assertEquals(200, status.getStatusLine().getStatusCode());
		List<BundleEntry> entries = ourCtx.newXmlParser().parseBundle(responseContent).getEntries();
		assertEquals(1, entries.size());
		Patient p = (Patient) entries.get(0).getResource();
		assertEquals("0123", p.getName().get(0).getFamilyFirstRep().getValue());
		assertEquals("1Organization", p.getName().get(1).getFamilyFirstRep().getValue());
		assertEquals("2", p.getName().get(2).getFamilyFirstRep().getValue());
	}

	@Test
	public void testSearchWithValueAndTypeAndChain() throws Exception {
		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?" + Patient.SP_PROVIDER + ":Organization.name=123");
		HttpResponse status = ourClient.execute(httpGet);
		String responseContent = IOUtils.toString(status.getEntity().getContent());
		IOUtils.closeQuietly(status.getEntity().getContent());

		assertEquals(200, status.getStatusLine().getStatusCode());
		List<BundleEntry> entries = ourCtx.newXmlParser().parseBundle(responseContent).getEntries();
		assertEquals(1, entries.size());
		Patient p = (Patient) entries.get(0).getResource();
		assertEquals("0123", p.getName().get(0).getFamilyFirstRep().getValue());
		assertEquals("1Organization", p.getName().get(1).getFamilyFirstRep().getValue());
		assertEquals("2name", p.getName().get(2).getFamilyFirstRep().getValue());

	}

	@AfterAll
	public static void afterClassClearContext() throws Exception {
		ourServer.stop();
		TestUtil.clearAllStaticFieldsForUnitTest();
	}

	@BeforeAll
	public static void beforeClass() throws Exception {
		ourPort = PortUtil.findFreePort();
		ourServer = new Server(ourPort);

		DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider();

		ServletHandler proxyHandler = new ServletHandler();
		RestfulServer servlet = new RestfulServer(ourCtx);
        servlet.setBundleInclusionRule(BundleInclusionRule.BASED_ON_RESOURCE_PRESENCE);
		servlet.setResourceProviders(patientProvider, new DummyOrganizationResourceProvider(), new DummyLocationResourceProvider());
		ServletHolder servletHolder = new ServletHolder(servlet);
		proxyHandler.addServletWithMapping(servletHolder, "/*");
		ourServer.setHandler(proxyHandler);
		ourServer.start();

		PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
		HttpClientBuilder builder = HttpClientBuilder.create();
		builder.setConnectionManager(connectionManager);
		ourClient = builder.build();
	}

	public static class DummyLocationResourceProvider implements IResourceProvider {

		@Override
		public Class<? extends IResource> getResourceType() {
			return Location.class;
		}

		//@formatter:off
		@Search
		public List<Location> searchByNameWithDifferentChain(
				@OptionalParam(name = "partof", chainWhitelist= {"bar"}) ReferenceParam theBarId) {
			//@formatter:on

			ArrayList<Location> retVal = new ArrayList<Location>();
			if (theBarId != null) {
				Location loc = new Location();
				loc.setId("1");
				loc.getName().setValue("theBarId " + theBarId.getValue() + " " + theBarId.getChain());
				retVal.add(loc);
			}
			if (retVal.isEmpty()) {
				ourLog.info("No values for bar - Going to fail");
				throw new InternalErrorException("No Values for bar");
			}

			return retVal;
		}

	}

	public static class DummyOrganizationResourceProvider implements IResourceProvider {

		@Override
		public Class<? extends IResource> getResourceType() {
			return Organization.class;
		}

		/**
		 * https://github.com/hapifhir/hapi-fhir/issues/19
		 */
		//@formatter:off
		@Search
		public List<Organization> searchByName(
				@OptionalParam(name = "partof", chainWhitelist= {"", "fooChain"}) ReferenceParam thePartOfId, 
				@OptionalParam(name = "partof.name") StringParam thePartOfName) {
			//@formatter:on

			ArrayList<Organization> retVal = new ArrayList<Organization>();
			if (thePartOfId != null) {
				Organization org = new Organization();
				org.setId("1");
				org.getName().setValue("thePartOfId " + thePartOfId.getValue() + " " + thePartOfId.getChain());
				retVal.add(org);
			}
			if (thePartOfName != null) {
				Organization org = new Organization();
				org.setId("2");
				org.getName().setValue("thePartOfName " + thePartOfName.getValue());
				retVal.add(org);
			}
			if (retVal.isEmpty()) {
				ourLog.info("No values for foo - Going to fail");
				throw new InternalErrorException("No Values for foo");
			}

			return retVal;
		}

		//@formatter:off
		@Search
		public List<Organization> searchByNameWithDifferentChain(
				@OptionalParam(name = "partof", chainWhitelist= {"bar"}) ReferenceParam theBarId) {
			//@formatter:on

			ArrayList<Organization> retVal = new ArrayList<Organization>();
			if (theBarId != null) {
				Organization org = new Organization();
				org.setId("1");
				org.getName().setValue("theBarId " + theBarId.getValue() + " " + theBarId.getChain());
				retVal.add(org);
			}
			if (retVal.isEmpty()) {
				ourLog.info("No values for bar - Going to fail");
				throw new InternalErrorException("No Values for bar");
			}

			return retVal;
		}

	}


	/**
	 * Created by dsotnikov on 2/25/2014.
	 */
	public static class DummyPatientResourceProvider implements IResourceProvider {

		private Patient createPatient() {
			Patient p = new Patient();
			p.setId("Patient/22/_history/33");
			p.addIdentifier("urn:foo", "findPatientWithVersion");

			Observation org = new Observation();
			org.setId("Observation/44/_history/55");
			p.setManagingOrganization(new ResourceReferenceDt(org));
			return p;
		}

		@Search
		public List<Patient> findPatient(@OptionalParam(name = Patient.SP_PROVIDER, targetTypes = { Organization.class }) ReferenceParam theParam) {
			ourLastRefParam = theParam;

			ArrayList<Patient> retVal = new ArrayList<Patient>();

			Patient p = new Patient();
			p.setId("1");
			p.addName().addFamily("0" + theParam.getValueAsQueryToken(ourCtx));
			p.addName().addFamily("1" + defaultString(theParam.getResourceType()));
			p.addName().addFamily("2" + defaultString(theParam.getChain()));
			retVal.add(p);

			return retVal;
		}

		@Search(queryName = "findPatientWithVersion")
		public List<Patient> findPatientWithVersion() {
			ArrayList<Patient> retVal = new ArrayList<Patient>();

			Patient p = createPatient();

			retVal.add(p);

			return retVal;
		}

		@Override
		public Class<? extends IResource> getResourceType() {
			return Patient.class;
		}

		@Read
		public Patient read(@IdParam IdDt theId) {
			return createPatient();
		}

	}

}
