/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.geode.internal.config;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.util.List;
import java.util.Set;

import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlType;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.RestoreSystemProperties;

import org.apache.geode.cache.configuration.CacheConfig;
import org.apache.geode.cache.configuration.CacheElement;
import org.apache.geode.cache.configuration.GatewayReceiverConfig;
import org.apache.geode.cache.configuration.RegionConfig;
import org.apache.geode.internal.lang.SystemPropertyHelper;
import org.apache.geode.management.configuration.RegionType;

public class JAXBServiceTest {

  private String xml;
  private CacheConfig unmarshalled;
  private JAXBService service;
  private JAXBService service2;

  @Rule
  public RestoreSystemProperties restore = new RestoreSystemProperties();

  @Before
  public void setUp() {
    service = new JAXBService(CacheConfig.class, ElementOne.class, ElementTwo.class);
    service.validateWithLocalCacheXSD();

    service2 = new JAXBService(CacheConfig.class);
    service2.validateWithLocalCacheXSD();
  }

  @Test
  public void getPackagesToScanWithoutSystemProperty() {
    Set<String> packages = JAXBService.getPackagesToScan();
    assertThat(packages).containsExactly("*");
  }

  @Test
  public void getPackagesToScanWithSystemProperty() {
    System.setProperty("geode." + SystemPropertyHelper.PACKAGES_TO_SCAN,
        "org.apache.geode,io.pivotal");
    Set<String> packages = JAXBService.getPackagesToScan();
    assertThat(packages).containsExactly("org.apache.geode", "io.pivotal");
  }

  @Test
  public void testCacheMarshall() {
    CacheConfig cacheConfig = new CacheConfig();
    setBasicValues(cacheConfig);

    xml = service.marshall(cacheConfig);
    System.out.println(xml);
    // cache has the default namespace
    assertThat(xml).contains("xmlns=\"http://geode.apache.org/schema/cache\"");
    assertThat(xml).contains(
        "xsi:schemaLocation=\"http://geode.apache.org/schema/cache http://geode.apache.org/schema/cache/cache-1.0.xsd\"");
    assertThat(xml).contains("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
    assertThat(xml).contains("</cache>");

    unmarshalled = service.unMarshall(xml);
  }

  @Test
  public void invalidXmlShouldFail() {
    CacheConfig cacheConfig = new CacheConfig();
    // missing version attribute
    assertThatThrownBy(() -> service.marshall(cacheConfig))
        .hasStackTraceContaining("Attribute 'version' must appear on element 'cache'");
  }

  @Test
  public void testCacheOneMarshall() {
    CacheConfig cache = new CacheConfig();
    setBasicValues(cache);
    cache.getCustomCacheElements().add(new ElementOne("test"));

    xml = service.marshall(cache);
    System.out.println(xml);
    // cache has the default namespace
    assertThat(xml).contains("xmlns=\"http://geode.apache.org/schema/cache\"");
    assertThat(xml).contains("custom-one>");
  }

  @Test
  public void testMixMarshall() {
    CacheConfig cache = new CacheConfig();
    setBasicValues(cache);
    cache.getCustomCacheElements().add(new ElementOne("testOne"));

    xml = service.marshall(cache);
    System.out.println(xml);
    assertThat(xml).contains("custom-one>");

    unmarshalled = service2.unMarshall(xml);
    unmarshalled.getCustomCacheElements().add(new ElementTwo("testTwo"));

    // xml generated wtih CacheConfigTwo has both elements in there.
    xml = service.marshall(unmarshalled);
    System.out.println(xml);
    assertThat(xml).contains("custom-one>");
    assertThat(xml).contains("custom-two>");
    assertThat(xml).containsPattern("xmlns=\"http://geode.apache.org/schema/cache\"");
    assertThat(xml).containsPattern("xmlns:ns\\d=\"http://geode.apache.org/schema/CustomOne\"");
    assertThat(xml).containsPattern("xmlns:ns\\d=\"http://geode.apache.org/schema/CustomTwo\"");
  }

  @Test
  public void xmlWithCustomElementsCanBeUnMarshalledByAnotherService() {
    CacheConfig cache = new CacheConfig();
    setBasicValues(cache);
    cache.getCustomCacheElements().add(new ElementOne("test"));
    cache.getCustomCacheElements().add(new ElementTwo("test"));

    String prettyXml = service.marshall(cache);
    System.out.println(prettyXml);

    CacheConfig cacheConfig = service2.unMarshall(prettyXml);
    List elements = cacheConfig.getCustomCacheElements();
    assertThat(elements.get(0)).isNotInstanceOf(ElementOne.class);
    assertThat(elements.get(1)).isNotInstanceOf(ElementTwo.class);

    String uglyXml = service2.marshall(cacheConfig);
    System.out.println(uglyXml);
    assertThat(uglyXml).isNotEqualTo(prettyXml);

    // the xml can be unmarshalled correctly by the first service
    String newXml = service.marshall(service.unMarshall(uglyXml));
    assertThat(newXml).isEqualTo(prettyXml);
  }

  @Test
  public void unmarshallPartialElement() {
    String xml = "<region name=\"one\">\n"
        + "        <region-attributes scope=\"distributed-ack\" data-policy=\"replicate\"/>\n"
        + "    </region>";

    RegionConfig config = service2.unMarshall(xml, RegionConfig.class);
    assertThat(config.getName()).isEqualTo("one");
  }

  @Test
  public void unmarshallAnyElement() {
    String xml = "<region name=\"one\">\n"
        + "        <region-attributes scope=\"distributed-ack\" data-policy=\"replicate\"/>\n"
        + "        <custom:any xmlns:custom=\"http://geode.apache.org/schema/custom\" id=\"any\"/>"
        + "    </region>";

    RegionConfig config = service2.unMarshall(xml, RegionConfig.class);
    assertThat(config.getName()).isEqualTo("one");
    assertThat(config.getCustomRegionElements()).hasSize(1);
  }

  @Test
  public void unmarshallIgnoresUnknownProperties() {
    // say xml has a type attribute that is removed in the new version
    String existingXML = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n"
        + "<cache version=\"1.0\" xsi:schemaLocation=\"http://geode.apache.org/schema/cache "
        + "http://geode.apache.org/schema/cache/cache-1.0.xsd\" "
        + "xmlns=\"http://geode.apache.org/schema/cache\" "
        + "xmlns:ns2=\"http://geode.apache.org/schema/CustomOne\" "
        + "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n" + "    <ns2:custom-one>\n"
        + "        <ns2:id>one</ns2:id>\n" + "        <ns2:type>onetype</ns2:type>\n"
        + "        <ns2:value>onevalue</ns2:value>\n" + "    </ns2:custom-one>\n" + "</cache>";

    CacheConfig cacheConfig = service.unMarshall(existingXML);
    List elements = cacheConfig.getCustomCacheElements();
    assertThat(elements.get(0)).isInstanceOf(ElementOne.class);
  }

  @Test
  public void marshalOlderNameSpace() {
    String xml =
        "<cache xsi:schemaLocation=\"http://schema.pivotal.io/gemfire/cache http://schema.pivotal.io/gemfire/cache/cache-8.1.xsd\"\n"
            +
            "       version=\"8.1\"\n" +
            "       xmlns=\"http://schema.pivotal.io/gemfire/cache\"\n" +
            "       xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n" +
            "    <region name=\"one\">\n" +
            "        <region-attributes scope=\"distributed-ack\" data-policy=\"replicate\"/>\n" +
            "    </region>\n" +
            "</cache>";

    CacheConfig cacheConfig = service2.unMarshall(xml);
    assertThat(cacheConfig.getRegions()).hasSize(1);
    assertThat(cacheConfig.getRegions().get(0).getName()).isEqualTo("one");
  }

  @Test
  public void marshallNoNameSpace() {
    String xml = "<cache version=\"1.0\">\n" +
        "    <region name=\"one\">\n" +
        "        <region-attributes scope=\"distributed-ack\" data-policy=\"replicate\"/>\n" +
        "    </region>\n" +
        "</cache>";

    CacheConfig cacheConfig = service2.unMarshall(xml);
    assertThat(cacheConfig.getRegions()).hasSize(1);
    assertThat(cacheConfig.getRegions().get(0).getName()).isEqualTo("one");
  }

  private void setBasicValues(CacheConfig cache) {
    GatewayReceiverConfig receiver = new GatewayReceiverConfig();
    receiver.setBindAddress("localhost");
    receiver.setEndPort("8080");
    receiver.setManualStart(false);
    receiver.setStartPort("6000");

    RegionConfig region = new RegionConfig();
    region.setName("testRegion");
    region.setType(RegionType.REPLICATE);

    cache.setCopyOnRead(true);
    cache.setGatewayReceiver(receiver);
    cache.setVersion("1.0");
    cache.getRegions().add(region);
  }

  @XmlAccessorType(XmlAccessType.FIELD)
  @XmlType(name = "", propOrder = {"id", "value"})
  @XmlRootElement(name = "custom-one", namespace = "http://geode.apache.org/schema/CustomOne")
  public static class ElementOne extends CacheElement {
    @XmlElement(name = "id", namespace = "http://geode.apache.org/schema/CustomOne")
    private String id;
    @XmlElement(name = "value", namespace = "http://geode.apache.org/schema/CustomOne")
    private String value;

    public ElementOne() {
      // nothing
    }

    public String getValue() {
      return value;
    }

    public void setValue(String value) {
      this.value = value;
    }

    public ElementOne(String id) {
      this.id = id;
    }

    @Override
    public String getId() {
      return id;
    }

    public void setId(String value) {
      id = value;
    }
  }

  @XmlAccessorType(XmlAccessType.FIELD)
  @XmlType(name = "", propOrder = {"id", "value"})
  @XmlRootElement(name = "custom-two", namespace = "http://geode.apache.org/schema/CustomTwo")
  public static class ElementTwo extends CacheElement {
    @XmlElement(name = "id", namespace = "http://geode.apache.org/schema/CustomTwo")
    private String id;
    @XmlElement(name = "value", namespace = "http://geode.apache.org/schema/CustomTwo")
    private String value;

    public ElementTwo() {
      // nothing
    }

    public String getValue() {
      return value;
    }

    public void setValue(String value) {
      this.value = value;
    }

    public ElementTwo(String id) {
      this.id = id;
    }

    @Override
    public String getId() {
      return id;
    }

    public void setId(String value) {
      id = value;
    }
  }

}
