/*
 *  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 groovy.xml

import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.assertThrows
import static org.junit.jupiter.api.Assertions.assertTrue

/**
 * The markup builders escape element text and attribute values, but
 * processing-instruction and comment bodies cannot use entity escaping.
 * These tests check that content which would close such a construct early
 * (and inject sibling markup) is rejected instead of emitted.
 */
final class MarkupBuilderInjectionTest {

    @Test
    void commentRejectsCommentTerminator() {
        assertThrows(IllegalArgumentException) {
            new MarkupBuilder(new StringWriter()).root { mkp.comment('legit --> <injected/>') }
        }
        assertThrows(IllegalArgumentException) {
            new MarkupBuilder(new StringWriter()).root { mkp.comment('a -- b') }
        }
    }

    @Test
    void commentKeepsPlainTextLiteral() {
        def writer = new StringWriter()
        new MarkupBuilder(writer).root { mkp.comment('plain > & < text') }
        assertTrue(writer.toString().contains('<!-- plain > & < text -->'))
    }

    @Test
    void streamingProcessingInstructionRejectsTerminator() {
        def smb = new StreamingMarkupBuilder()
        assertThrows(GroovyRuntimeException) {
            smb.bind { mkp.pi('xml-stylesheet': [href: 'a', type: 'b?><injected/>']); root('x') }.toString()
        }
    }

    @Test
    void streamingProcessingInstructionKeepsPlainData() {
        def smb = new StreamingMarkupBuilder()
        def result = smb.bind { mkp.pi('xml-stylesheet': [href: 'style.css', type: 'text/css']); root('x') }.toString()
        assertTrue(result.contains('<?xml-stylesheet') && result.contains('style.css') && result.contains('?>'))
    }

    @Test
    void streamingProcessingInstructionRejectsTerminatorInTarget() {
        def smb = new StreamingMarkupBuilder()
        assertThrows(GroovyRuntimeException) {
            smb.bind { mkp.pi('x?><injected/>': [href: 'a']); root('x') }.toString()
        }
    }

    @Test
    void streamingProcessingInstructionRejectsTerminatorInNonMapData() {
        def smb = new StreamingMarkupBuilder()
        assertThrows(GroovyRuntimeException) {
            smb.bind { mkp.pi('target': 'data?><injected/>'); root('x') }.toString()
        }
    }

    @Test
    void streamingProcessingInstructionKeepsPlainNonMapData() {
        def smb = new StreamingMarkupBuilder()
        def result = smb.bind { mkp.pi('target': 'plain data'); root('x') }.toString()
        assertTrue(result.contains('<?target plain data?>'))
    }

    @Test
    void streamingCommentRejectsCommentTerminator() {
        def smb = new StreamingMarkupBuilder()
        assertThrows(GroovyRuntimeException) {
            smb.bind { mkp.comment('legit --> <injected/> rest'); root('x') }.toString()
        }
        assertThrows(GroovyRuntimeException) {
            smb.bind { mkp.comment('a -- b'); root('x') }.toString()
        }
        assertThrows(GroovyRuntimeException) {
            smb.bind { mkp.comment('ends with dash-'); root('x') }.toString()
        }
    }

    @Test
    void streamingCommentKeepsPlainText() {
        def smb = new StreamingMarkupBuilder()
        def result = smb.bind { mkp.comment('plain text'); root('x') }.toString()
        assertTrue(result.contains('<!--plain text-->'))
    }

    @Test
    void streamingDomProcessingInstructionRejectsTerminator() {
        assertThrows(GroovyRuntimeException) {
            new StreamingDOMBuilder().bind { mkp.pi('xml-stylesheet': [href: 'a?><injected/>']); root('x') }()
        }
        assertThrows(GroovyRuntimeException) {
            new StreamingDOMBuilder().bind { mkp.pi('x?><injected/>': [href: 'a']); root('x') }()
        }
        assertThrows(GroovyRuntimeException) {
            new StreamingDOMBuilder().bind { mkp.pi('target': 'data?><injected/>'); root('x') }()
        }
    }

    @Test
    void streamingDomCommentRejectsCommentTerminator() {
        assertThrows(GroovyRuntimeException) {
            new StreamingDOMBuilder().bind { mkp.comment('a -- b'); root('x') }()
        }
    }

    /** Serialising SAX handler, so the emitted document can be inspected as text. */
    private static saxHandlerTo(ByteArrayOutputStream out) {
        def handler = javax.xml.transform.TransformerFactory.newInstance().newTransformerHandler()
        handler.setResult(new javax.xml.transform.stream.StreamResult(out))
        handler
    }

    // GROOVY-12338
    @Test
    void streamingSaxCommentRejectsCommentTerminator() {
        assertThrows(GroovyRuntimeException) {
            new StreamingSAXBuilder().bind { mkp.comment('legit --> <injected/>'); root('x') }(saxHandlerTo(new ByteArrayOutputStream()))
        }
        assertThrows(GroovyRuntimeException) {
            new StreamingSAXBuilder().bind { mkp.comment('a -- b'); root('x') }(saxHandlerTo(new ByteArrayOutputStream()))
        }
        assertThrows(GroovyRuntimeException) {
            new StreamingSAXBuilder().bind { mkp.comment('trailing hyphen-'); root('x') }(saxHandlerTo(new ByteArrayOutputStream()))
        }
    }

    // GROOVY-12338
    @Test
    void streamingSaxCommentIsCheckedEvenWhenTheHandlerWouldDropIt() {
        // a handler that is not a LexicalHandler discards comments; malformed content should still
        // be reported rather than silently thrown away
        def plainHandler = new org.xml.sax.helpers.DefaultHandler()
        assertThrows(GroovyRuntimeException) {
            new StreamingSAXBuilder().bind { mkp.comment('a -- b'); root('x') }(plainHandler)
        }
    }

    // GROOVY-12338
    @Test
    void streamingSaxProcessingInstructionRejectsTerminator() {
        assertThrows(GroovyRuntimeException) {
            new StreamingSAXBuilder().bind { mkp.pi('xml-stylesheet': [href: 'a?><injected/>']); root('x') }(saxHandlerTo(new ByteArrayOutputStream()))
        }
        assertThrows(GroovyRuntimeException) {
            new StreamingSAXBuilder().bind { mkp.pi('x?><injected/>': [href: 'a']); root('x') }(saxHandlerTo(new ByteArrayOutputStream()))
        }
        assertThrows(GroovyRuntimeException) {
            new StreamingSAXBuilder().bind { mkp.pi('target': 'data?><injected/>'); root('x') }(saxHandlerTo(new ByteArrayOutputStream()))
        }
    }

    // GROOVY-12338
    @Test
    void streamingSaxKeepsPlainCommentAndProcessingInstruction() {
        def out = new ByteArrayOutputStream()
        new StreamingSAXBuilder().bind {
            mkp.pi('xml-stylesheet': [href: 'style.css', type: 'text/css'])
            root { mkp.comment('plain > & < text') }
        }(saxHandlerTo(out))
        def result = out.toString()

        assertTrue(result.contains('style.css'), result)
        assertTrue(result.contains('plain'), result)
    }
}
