package study.springcaps;

import org.junit.jupiter.api.Test;
import org.springframework.oxm.MarshallingFailureException;
import org.springframework.oxm.UnmarshallingFailureException;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;

import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * U49 OXM：真实 Jaxb2Marshaller（jakarta JAXB）行为与失败边界。
 * 观察行前缀 U49，与 run.py 的 MARKERS 逐条对应。
 */
class JaxbOxmTest {

    static Order sample() {
        return new Order("CNY", "O-1", List.of(
                new Order.Item("SKU-A", 2, new BigDecimal("19.90")),
                new Order.Item("SKU-B", 1, new BigDecimal("200.00"))));
    }

    @Test
    void oxmRoundTripAndSupports() {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        marshaller.setClassesToBeBound(Order.class, Order.Item.class);
        try {
            marshaller.afterPropertiesSet();
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }

        assertTrue(marshaller.supports(Order.class), "根元素类必须被支持");
        assertFalse(marshaller.supports(String.class), "非映射类型不应被支持");

        javax.xml.transform.stream.StreamResult out = new javax.xml.transform.stream.StreamResult(new java.io.StringWriter());
        marshaller.marshal(sample(), out);
        String xml = ((java.io.StringWriter) out.getWriter()).toString();
        assertTrue(xml.contains("<order currency=\"CNY\">"), "根元素与属性应按注解输出");
        assertTrue(xml.contains("<price>19.90</price>"), "BigDecimal 按其字符串形式输出（保留 19.90）");

        Order back = (Order) marshaller.unmarshal(new javax.xml.transform.stream.StreamSource(new StringReader(xml)));
        assertEquals("O-1", back.getId());
        assertEquals(2, back.getItems().size());
        assertEquals("SKU-B", back.getItems().get(1).getSku());

        // 非根元素类直接 marshal 抛 MarshallingFailureException
        assertThrows(MarshallingFailureException.class, () -> marshaller.marshal("not-a-mapped-object", new javax.xml.transform.stream.StreamResult(new java.io.StringWriter())));

        System.out.println("U49 oxmRoundTrip=PASS root=order currencyAttr=true supportsCheck=true");
    }

    @Test
    void oxmDtdAndExternalEntityDefaults() throws IOException {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        marshaller.setClassesToBeBound(Order.class);
        try {
            marshaller.afterPropertiesSet();
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
        assertEquals(false, marshaller.isSupportDtd(), "DTD 默认关闭");
        assertEquals(false, marshaller.isProcessExternalEntities(), "外部实体默认不解析");

        // XXE 注入：DOCTYPE 声明默认被拒绝（disallow-doctype-decl）
        String xxe = """
                <?xml version="1.0"?>
                <!DOCTYPE order [<!ENTITY xxe SYSTEM "file:///etc/hostname">]>
                <order currency="CNY"><id>&xxe;</id></order>
                """;
        StreamSource evil = new StreamSource(new ByteArrayInputStream(xxe.getBytes(StandardCharsets.UTF_8)));
        Exception failure = null;
        try {
            marshaller.unmarshal(evil);
        } catch (Exception ex) {
            failure = ex;
        }
        assertTrue(failure != null, "DOCTYPE 输入默认必须失败");
        System.out.println("U49 oxDtdReject=PASS exception=" + failure.getClass().getSimpleName());

        // 坏 XML（截断）→ UnmarshallingFailureException
        String truncated = "<order currency=\"CNY\"><id>o";
        UnmarshallingFailureException bad = assertThrows(UnmarshallingFailureException.class,
                () -> marshaller.unmarshal(new StreamSource(new ByteArrayInputStream(truncated.getBytes(StandardCharsets.UTF_8)))));
        System.out.println("U49 oxmBadXml=PASS wrapped=" + bad.getClass().getSimpleName());

        // 打开 DTD 支持（教学对照）：同样输入仍然因实体未解析而不同——外部实体默认 false
        marshaller.setSupportDtd(true);
        String dtdNoEntity = """
                <?xml version="1.0"?>
                <!DOCTYPE order>
                <order currency="CNY"><id>OK-DTD</id></order>
                """;
        Order ok = (Order) marshaller.unmarshal(new StreamSource(new ByteArrayInputStream(dtdNoEntity.getBytes(StandardCharsets.UTF_8))));
        assertEquals("OK-DTD", ok.getId(), "仅 DOCTYPE 无实体时，开 DTD 后可解");
        System.out.println("U49 oxmDtdToggle=PASS dtdNoEntity=ok");
    }

    @Test
    void oxmSchemaValidationFailure() throws Exception {
        // XSD 校验：id 少于 2 字符时失败，错误转成 ValidationFailureException 层次
        String xsd = """
                <?xml version="1.0"?>
                <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
                  <xs:element name="order">
                    <xs:complexType>
                      <xs:sequence>
                        <xs:element name="id">
                          <xs:simpleType>
                            <xs:restriction base="xs:string"><xs:minLength value="2"/></xs:restriction>
                          </xs:simpleType>
                        </xs:element>
                        <xs:element name="item" minOccurs="0" maxOccurs="unbounded">
                          <xs:complexType>
                            <xs:sequence>
                              <xs:element name="quantity" type="xs:int"/>
                              <xs:element name="price" type="xs:decimal"/>
                            </xs:sequence>
                            <xs:attribute name="sku" type="xs:string"/>
                          </xs:complexType>
                        </xs:element>
                      </xs:sequence>
                      <xs:attribute name="currency" type="xs:string"/>
                    </xs:complexType>
                  </xs:element>
                </xs:schema>
                """;
        java.nio.file.Path xsdFile = java.nio.file.Files.createTempFile("u49-order", ".xsd");
        java.nio.file.Files.writeString(xsdFile, xsd, StandardCharsets.UTF_8);
        try {
            Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
            marshaller.setClassesToBeBound(Order.class, Order.Item.class);
            marshaller.setSchema(new org.springframework.core.io.FileSystemResource(xsdFile.toFile()));
            marshaller.afterPropertiesSet();

            String invalid = "<order currency=\"CNY\"><id>x</id></order>";
            org.springframework.oxm.XmlMappingException mappingEx = assertThrows(org.springframework.oxm.XmlMappingException.class,
                    () -> marshaller.unmarshal(new StreamSource(new ByteArrayInputStream(invalid.getBytes(StandardCharsets.UTF_8)))),
                    "XSD 违例必须落在 XmlMappingException 层次");
            // 7.0.10 实测：schema 校验错误经 UnmarshalException 包装为 UnmarshallingFailureException，
            // 不是 ValidationFailureException——教学观察以真实异常类型为准
            System.out.println("U49 oxmSchemaValidation=PASS invalidMinLength=rejected type=" + mappingEx.getClass().getSimpleName());

            String valid = "<order currency=\"CNY\"><id>OK</id><item sku=\"S\"><quantity>1</quantity><price>1.5</price></item></order>";
            Order ok = (Order) marshaller.unmarshal(new StreamSource(new ByteArrayInputStream(valid.getBytes(StandardCharsets.UTF_8))));
            assertEquals("OK", ok.getId());
        } finally {
            java.nio.file.Files.deleteIfExists(xsdFile);
        }
    }

    @Test
    void oxmJacksonXmlContrast() throws IOException {
        // Jackson 2 XML dataformat 对照：同一模型换映射技术
        com.fasterxml.jackson.dataformat.xml.XmlMapper mapper = new com.fasterxml.jackson.dataformat.xml.XmlMapper();
        String xml = mapper.writeValueAsString(sample());
        assertTrue(xml.contains("<currency>CNY</currency>"), "Jackson XML 把 @XmlAttribute 字段写为元素（默认无 JAXB 注解语义）");
        Order back = mapper.readValue(xml, Order.class);
        assertEquals("O-1", back.getId());

        // Jackson 默认不启用 XXE 防护开关的 JAXB 那套 feature；这里验证未知字段容忍
        String withUnknown = "<Order><currency>CNY</currency><id>O-2</id><unknownTag>1</unknownTag></Order>";
        Order tolerated = mapper.reader().without(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                .readValue(withUnknown, Order.class);
        assertEquals("O-2", tolerated.getId());
        System.out.println("U49 jacksonXmlContrast=PASS attributeAsElement=true unknownTolerated=true");
    }
}
