package ru.ibs.socrates.confexporter.service;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.database.rider.core.api.configuration.DBUnit;
import com.github.database.rider.core.api.configuration.Orthography;
import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.junit5.api.DBRider;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.dbunit.ext.postgresql.PostgresqlDataTypeFactory;
import org.json.JSONException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.skyscreamer.jsonassert.Customization;
import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.skyscreamer.jsonassert.comparator.CustomComparator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.servlet.MvcResult;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import ru.ibs.socrates.confexporter.dao.repository.MessageRepository;
import ru.ibs.socrates.confexporter.dao.repository.ProjectRepository;
import ru.ibs.socrates.confexporter.dao.repository.TextContentRepository;
import ru.ibs.socrates.confexporter.dao.service.MessageDaoService;
import ru.ibs.socrates.confexporter.external.chanker.model.DocumentRequest;
import ru.ibs.socrates.confexporter.external.chanker.model.DocumentResponse;
import ru.ibs.socrates.confexporter.external.pageparser.client.PageparserApiClient;
import ru.ibs.socrates.confexporter.external.pageparser.model.ParserRequest;
import ru.ibs.socrates.confexporter.external.pageparser.model.ParserResponse;
import ru.ibs.socrates.confexporter.external.sdtllmraggateconf.api.ContentApi;
import ru.ibs.socrates.confexporter.external.sdtllmraggateconf.api.SourceApi;
import ru.ibs.socrates.confexporter.external.sdtllmraggateconf.api.TextApi;
import ru.ibs.socrates.confexporter.external.sdtllmraggateconf.model.ContentHttpViewModel;
import ru.ibs.socrates.confexporter.external.sdtllmraggateconf.model.ReadTextHttpResponse;
import ru.ibs.socrates.confexporter.external.sdtllmraggateconf.model.SourceHttpViewModel;
import ru.ibs.socrates.confexporter.external.sdtllmraggateconf.rabbitmq.dto.SdtLlmRagGateEventDto;
import ru.ibs.socrates.confexporter.external.xmlparser.client.XmlparserApiClient;
import ru.ibs.socrates.confexporter.external.xmlparser.model.XmlParserDocumentRequest;
import ru.ibs.socrates.confexporter.external.xmlparser.model.XmlParserSchemaProcessResponse;
import ru.ibs.socrates.confexporter.service.content.service.ProcessContentService;


@Slf4j
@ActiveProfiles({"disable-feign-client", "test"})
@SpringBootTest
@Testcontainers
@DBRider
@DBUnit(allowEmptyFields = true, cacheConnection = false, columnSensing = true,
    caseSensitiveTableNames = false, caseInsensitiveStrategy = Orthography.LOWERCASE,
    mergeDataSets = true,
    dataTypeFactoryClass = PostgresqlDataTypeFactory.class)
public class ProcessContentServiceImplTest {


  @Container
  private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
      .withDatabaseName("testdb")
      .withUsername("test")
      .withPassword("test");

  @DynamicPropertySource
  static void postgresqlProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", postgres::getJdbcUrl);
    registry.add("spring.datasource.password", postgres::getPassword);
    registry.add("spring.datasource.username", postgres::getUsername);

  }

  @Autowired
  private ProcessContentService processContentService;

  @Autowired
  private ObjectMapper objectMapper;
  
  @Autowired
  private PageparserApiClient pageparserApiClient;

  @Autowired
  private XmlparserApiClient xmlparserApiClient;
  
  @Autowired
  private MessageRepository contentRepository;
  
  @Autowired
  private TextContentRepository textContentRepository;
  
  @Autowired
  private ProjectRepository projectRepository;
  
  @Autowired
  private MessageDaoService contentDaoService;
  
  @Autowired
  private SourceApi sourceApi;
  
  @Autowired
  private ContentApi contentApi;

  @Autowired
  private TextApi textApi;

  @Autowired
  private ru.ibs.socrates.confexporter.external.chanker.api.DefaultApi chankerApi;

  @Autowired
  private ru.ibs.socrates.confexporter.external.xmlparser.api.DefaultApi xmlParserApi;

  @BeforeEach
  void init() throws IOException {
    Mockito.reset(contentApi, textApi, chankerApi, pageparserApiClient, xmlparserApiClient);
    projectRepository.deleteAll();
    contentRepository.deleteAll();
    textContentRepository.deleteAll();
    
    mockSourceApi("/data/process-content-test/test-001/source-response.json");
    doAnswer(invocation -> {
      ParserRequest parserRequest = (ParserRequest) invocation.getArguments()[0];
      String rawContent  = Optional.ofNullable(parserRequest)
          .map(ParserRequest::getRawContent)
          .map(content -> "PAGE PARSER: " + content)
          .orElse(null);
      ParserResponse response = new ParserResponse(rawContent);
      return new ResponseEntity<>(response, HttpStatus.OK);
    }).when(pageparserApiClient).parsedTextParsedTextPost(any(ParserRequest.class));
  }


  @DisplayName("Page Test")
  @Test
  void test001() throws IOException {

    String testResourceFolder = "/data/process-content-test/test-001";


    mockContentApi(testResourceFolder + "/content-response.json");
    mockTextApi(testResourceFolder + "/text-response.json");
    mockChankerApi(testResourceFolder + "/chanker-response.json");

    var request = getResource(testResourceFolder + "/request.json", 
        SdtLlmRagGateEventDto.class);
    processContentService.processContent(request);
    
    ArgumentCaptor<String> sourceCapture =
        ArgumentCaptor.forClass(String.class);
    verify(sourceApi, atLeast(1)).apiV1SourceSourceIdReadGet(sourceCapture.capture());
    Assertions.assertEquals("4d1a8a90-fdb6-4cff-bb89-83dafe62c35c", sourceCapture.getValue());

    ArgumentCaptor<DocumentRequest> chankerCapture =
        ArgumentCaptor.forClass(DocumentRequest.class);
    verify(chankerApi, atLeast(1)).parseDocumentProcessTextPost(chankerCapture.capture());
    
    assertJson(testResourceFolder + "/chanker-request.json",
        "**.id").accept(chankerCapture.getValue());
    
    Assertions.assertEquals(1, textContentRepository.count());
    Assertions.assertEquals("https://conf.ibs.ru/pages/viewpage.action?pageId=391061420",
        textContentRepository.findAll().get(0).getLink());
    assertJson(testResourceFolder + "/content-result.json",
        "**.id", "**.contentId", "**.updated").accept(contentDaoService.findTop20());
    
    
    Assertions.assertEquals(633, contentRepository.findAll().get(0).getContentSize());
    Assertions.assertNotNull(contentRepository.findAll().get(0).getCreated());
    Assertions.assertNotNull(contentRepository.findAll().get(0).getUpdated());
  }


  @DisplayName("File Test")
  @Test
  void test002() throws IOException {
    String testResourceFolder = "/data/process-content-test/test-002";

    mockContentApi(testResourceFolder + "/content-response.json");
    mockTextApi(testResourceFolder + "/text-response.json");
    mockChankerApi(testResourceFolder + "/chanker-response.json");

    var request = getResource(testResourceFolder + "/request.json",
        SdtLlmRagGateEventDto.class);
    processContentService.processContent(request);

    ArgumentCaptor<DocumentRequest> chankerCapture =
        ArgumentCaptor.forClass(DocumentRequest.class);
    verify(chankerApi, atLeast(1)).parseDocumentProcessTextPost(chankerCapture.capture());

    assertJson(testResourceFolder + "/chanker-request.json",
        "**.id").accept(chankerCapture.getValue());
    
    assertJson(testResourceFolder + "/content-result.json",
        "**.id", "**.contentId", "**.updated").accept(contentDaoService.findTop20());
  }


  @DisplayName("Page Test 3")
  @Test
  void test003() throws IOException {
    String testResourceFolder = "/data/process-content-test/test-003";

    mockContentApi(testResourceFolder + "/content-response.json");
    mockTextApi(testResourceFolder + "/text-response.json");
    mockChankerApi(testResourceFolder + "/chanker-response.json");

    var request = getResource(testResourceFolder + "/request.json",
        SdtLlmRagGateEventDto.class);
    processContentService.processContent(request);

    ArgumentCaptor<DocumentRequest> chankerCapture =
        ArgumentCaptor.forClass(DocumentRequest.class);
    verify(chankerApi, atLeast(1)).parseDocumentProcessTextPost(chankerCapture.capture());

    assertJson(testResourceFolder + "/chanker-request.json",
        "**.id").accept(chankerCapture.getValue());
    
    assertJson(testResourceFolder + "/content-result.json",
        "**.id", "**.contentId", "**.updated").accept(contentDaoService.findTop20());
  }


  @DisplayName("Folder Test 4")
  @Test
  void test004() throws IOException {
    String testResourceFolder = "/data/process-content-test/test-004";

    mockContentApi(testResourceFolder + "/content-response.json");
    mockTextApi(testResourceFolder + "/text-response.json");
    mockChankerApi(testResourceFolder + "/chanker-response.json");

    var request = getResource(testResourceFolder + "/request.json",
        SdtLlmRagGateEventDto.class);
    processContentService.processContent(request);

    ArgumentCaptor<DocumentRequest> chankerCapture =
        ArgumentCaptor.forClass(DocumentRequest.class);
    verify(chankerApi, times(0)).parseDocumentProcessTextPost(chankerCapture.capture());
    
    assertJson(testResourceFolder + "/content-result.json",
        "**.id", "**.contentId").accept(contentDaoService.findTop20());
  }


  @DisplayName("Xml Test 5")
  @Test
  void test005() throws IOException {
    String testResourceFolder = "/data/process-content-test/test-005";

    mockContentApi(testResourceFolder + "/content-response.json");
    mockTextApi(testResourceFolder + "/text-response.json");
    mockChankerApi(testResourceFolder + "/chanker-response.json");

    var request = getResource(testResourceFolder + "/request.json",
        SdtLlmRagGateEventDto.class);
    processContentService.processContent(request);

    ArgumentCaptor<DocumentRequest> chankerCapture =
        ArgumentCaptor.forClass(DocumentRequest.class);
    verify(chankerApi, atLeast(1)).parseDocumentProcessTextPost(chankerCapture.capture());

    assertJson(testResourceFolder + "/chanker-request.json",
        "**.id").accept(chankerCapture.getValue());
    
    assertJson(testResourceFolder + "/content-result.json",
        "**.id", "**.contentId", "**.updated").accept(contentDaoService.findTop20());
  }


  @DisplayName("Xml Test 6")
  @Test
  void test006() throws IOException {
    String testResourceFolder = "/data/process-content-test/test-006";

    mockContentApi(testResourceFolder + "/content-response.json");
    mockTextApi(testResourceFolder + "/text-response.json");
    mockChankerApi(testResourceFolder + "/chanker-response.json");

    var request = getResource(testResourceFolder + "/request.json",
        SdtLlmRagGateEventDto.class);
    processContentService.processContent(request);

    ArgumentCaptor<DocumentRequest> chankerCapture =
        ArgumentCaptor.forClass(DocumentRequest.class);
    verify(chankerApi, atLeast(1)).parseDocumentProcessTextPost(chankerCapture.capture());

    assertJson(testResourceFolder + "/chanker-request.json",
        "**.id").accept(chankerCapture.getValue());
    
    assertJson(testResourceFolder + "/content-result.json",
        "**.id", "**.contentId", "**.updated").accept(contentDaoService.findTop20());
  }


  @DisplayName("007 existing row")
  @Test
  @DataSet(value = {
      "data/process-content-test/test-007/project.xml",
      "data/process-content-test/test-007/message.xml",
      "data/process-content-test/test-007/text_content.xml",
  })
  void test007() throws IOException {

    String testResourceFolder = "/data/process-content-test/test-007";


    mockContentApi(testResourceFolder + "/content-response.json");
    mockTextApi(testResourceFolder + "/text-response.json");
    mockChankerApi(testResourceFolder + "/chanker-response.json");

    var request = getResource(testResourceFolder + "/request.json", 
        SdtLlmRagGateEventDto.class);
    processContentService.processContent(request);

    ArgumentCaptor<DocumentRequest> chankerCapture =
        ArgumentCaptor.forClass(DocumentRequest.class);
    verify(chankerApi, atLeast(1)).parseDocumentProcessTextPost(chankerCapture.capture());
    
    assertJson(testResourceFolder + "/chanker-request.json",
        "**.id").accept(chankerCapture.getValue());
    
    assertJson(testResourceFolder + "/content-result.json",
        "**.id", "**.contentId", "**.updated").accept(contentDaoService.findTop20());
    Assertions.assertEquals(1, textContentRepository.count());
  }
  

  @DisplayName("008 existing project")
  @Test
  @DataSet(value = {
      "data/process-content-test/test-008/project.xml",
      "data/process-content-test/test-008/message.xml",
      "data/process-content-test/test-008/text_content.xml",
  })
  void test008() throws IOException {

    String testResourceFolder = "/data/process-content-test/test-008";


    mockContentApi(testResourceFolder + "/content-response.json");
    mockTextApi(testResourceFolder + "/text-response.json");
    mockChankerApi(testResourceFolder + "/chanker-response.json");

    var request = getResource(testResourceFolder + "/request.json", 
        SdtLlmRagGateEventDto.class);
    processContentService.processContent(request);

    ArgumentCaptor<DocumentRequest> chankerCapture =
        ArgumentCaptor.forClass(DocumentRequest.class);
    verify(chankerApi, atLeast(1)).parseDocumentProcessTextPost(chankerCapture.capture());
    
    assertJson(testResourceFolder + "/chanker-request.json",
        "**.id").accept(chankerCapture.getValue());
    
    assertJson(testResourceFolder + "/content-result.json",
        "**.id", "**.contentId", "**.updated").accept(contentDaoService.findTop20());
  }

  @DisplayName("009 xml mediatype drawio")
  @Test
  void test009() throws IOException {
    String testResourceFolder = "/data/process-content-test/test-009";
    mockContentApi(testResourceFolder + "/content-response.json");
    mockTextApi(testResourceFolder + "/text-response.json");
    mockXmlParserApi(testResourceFolder + "/xml-parser-response.json");

    var request = getResource(testResourceFolder + "/request.json",
        SdtLlmRagGateEventDto.class);
    processContentService.processContent(request);

    ArgumentCaptor<XmlParserDocumentRequest> xmlParserCapture =
        ArgumentCaptor.forClass(XmlParserDocumentRequest.class);

    verify(xmlParserApi, atLeast(1))
        .processSchemaProcessSchemaPost(xmlParserCapture.capture());


    assertJson(testResourceFolder + "/xml-parser-request.json")
        .accept(xmlParserCapture.getValue());
  }

  public <T> T getResource(String resourceName, Class<T> clazz) {
    T res = null;
    try (Reader json = new InputStreamReader(
        ProcessContentServiceImplTest.class.getResourceAsStream(resourceName))) {
      res = objectMapper.readValue(json, clazz);
    } catch (Exception e) {
      log.error("unable to get resource {}", resourceName);
      Assertions.assertTrue(false, "unable to get resource " + resourceName);
    }
    return res;
  }


  /**
   * sourceApi
   */
  public void mockSourceApi(String dataPath) throws IOException {
    try (InputStream response = this.getClass()
        .getResourceAsStream(dataPath)) {
      var authPayload = objectMapper.readValue(response, SourceHttpViewModel.class);
      var responseEntity = new ResponseEntity<SourceHttpViewModel>(authPayload, HttpStatus.OK);

      doReturn(responseEntity).when(sourceApi)
          .apiV1SourceSourceIdReadGet(any(String.class));
    }
  }

  /**
   * contentApi
   */
  public void mockContentApi(String dataPath) throws IOException {
    try (InputStream response = this.getClass()
        .getResourceAsStream(dataPath)) {
      var authPayload = objectMapper.readValue(response, ContentHttpViewModel.class);
      var responseEntity = new ResponseEntity<ContentHttpViewModel>(authPayload, HttpStatus.OK);

      doReturn(responseEntity).when(contentApi)
          .apiV1ContentIdReadGet(any(String.class));
    }
  }

  /**
   * contentApi
   */
  public void mockTextApi(String dataPath) throws IOException {
    try (InputStream response = this.getClass()
        .getResourceAsStream(dataPath)) {
      var authPayload = objectMapper.readValue(response, ReadTextHttpResponse.class);
      var responseEntity = new ResponseEntity<ReadTextHttpResponse>(authPayload, HttpStatus.OK);

      doReturn(responseEntity).when(textApi)
          .apiV1TextIdReadGet(any(String.class));
    }
  }

  /**
   * contentApi
   */
  public void mockChankerApi(String dataPath) throws IOException {
    try (InputStream response = this.getClass()
        .getResourceAsStream(dataPath)) {
      var authPayload = objectMapper.readValue(response, DocumentResponse.class);
      var responseEntity = new ResponseEntity<DocumentResponse>(authPayload, HttpStatus.OK);

      doReturn(responseEntity).when(chankerApi)
          .parseDocumentProcessTextPost(any(DocumentRequest.class));
    }
  }

  /**
   * contentApi
   */
  public void mockXmlParserApi(String dataPath) throws IOException {
    try (InputStream response = this.getClass()
        .getResourceAsStream(dataPath)) {
      var authPayload = objectMapper.readValue(response, XmlParserSchemaProcessResponse.class);
      var responseEntity =
          new ResponseEntity<XmlParserSchemaProcessResponse>(authPayload, HttpStatus.OK);

      doReturn(responseEntity).when(xmlParserApi)
          .processSchemaProcessSchemaPost(any(XmlParserDocumentRequest.class));
    }
  }

  protected <T> Consumer<T> assertJson(String filePathExpected, String... skipField) {
    String expectedJson = getFileData(filePathExpected);
    Consumer<T> func = (obj) -> {
      String jsonActual;
      try {
        if (obj instanceof String) {
          jsonActual = (String) obj;
        } else if (obj instanceof byte[]) {
          jsonActual = new String((byte[]) obj, StandardCharsets.UTF_8);
        } else if (obj instanceof MvcResult) {
          jsonActual = getMvcResultJson((MvcResult) obj);
        } else {
          jsonActual = toJson(obj);
        }
        Customization[] customizations = Arrays.stream(skipField)
            .map(field -> new Customization(field, (a, b) -> true))
            .toArray(Customization[]::new);
        JSONAssert.assertEquals("filepath = " + filePathExpected
            + ", actual json = " + jsonActual + ", ->",
            expectedJson,
            jsonActual,
            new CustomComparator(JSONCompareMode.STRICT_ORDER,
                customizations
            ));
      } catch (JSONException | UnsupportedEncodingException e) {
        throw (RuntimeException) ExceptionUtils.rethrow(e);
      }
    };
    return func;
  }
  

  protected String getFileData(String filePath) {
    try (InputStream inputStream = this.getClass().getResourceAsStream(filePath)) {
      return new BufferedReader(new InputStreamReader(inputStream))
          .lines().collect(Collectors.joining("\n"));
    } catch (Exception e) {
      Assertions.assertEquals("", filePath, "filepath = " + filePath + " not found");
      throw (RuntimeException) ExceptionUtils.rethrow(e);
    }
  }
  

  protected String getMvcResultJson(MvcResult mvcResult) throws UnsupportedEncodingException {
    return mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
  }

  @SneakyThrows
  public String toJson(Object body) {
    if (Objects.isNull(body)) {
      return StringUtils.EMPTY;
    }
    return objectMapper.writeValueAsString(body);
  }
}
