package net.sergeych.toread.fb2

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class Fb2FormatTest {
    @Test
    fun parsesPlainXml() {
        val book = Fb2Format.parseXml(sampleXml)

        assertEquals("The Test Book", book.title)
        assertEquals("Ada Lovelace", book.authors.single().displayName)
        assertEquals("en", book.language)
        assertEquals(listOf("sf"), book.genres)
        assertEquals("Chapter 1", book.sections.single().title)
        assertEquals(listOf("Hello & welcome."), book.sections.single().paragraphs)
    }

    @Test
    fun exportsXmlThatCanBeParsedAgain() {
        val book = Fb2Book(
            title = "Exported",
            authors = listOf(Fb2Author(firstName = "Ann", lastName = "Writer")),
            language = "en",
            genres = listOf("prose"),
            sections = listOf(Fb2Section(title = "Start", paragraphs = listOf("One < two & three."))),
        )

        val reparsed = Fb2Format.parseXml(Fb2Format.exportXml(book))

        assertEquals(book.title, reparsed.title)
        assertEquals(book.authors.single().displayName, reparsed.authors.single().displayName)
        assertEquals(book.sections.single().paragraphs, reparsed.sections.single().paragraphs)
    }

    @Test
    fun parsesStoredZip() {
        val zip = Fb2Format.exportZip(Fb2Format.parseXml(sampleXml), "sample.fb2")
        val book = Fb2Format.parse(zip, "sample.fb2.zip")

        assertEquals("The Test Book", book.title)
        assertTrue(zip.copyOfRange(0, 4).contentEquals(byteArrayOf(0x50, 0x4b, 0x03, 0x04)))
    }

    @Test
    fun parsesWindows1251PlainXml() {
        val book = Fb2Format.parse(windows1251Xml.encodeWindows1251(), "legacy.fb2")

        assertEquals("Тестовая книга", book.title)
        assertEquals("Привет, мир.", book.sections.single().paragraphs.single())
    }

    @Test
    fun parsesWindows1251StoredZip() {
        val zip = Fb2Zip.createStoredZip("legacy.fb2", windows1251Xml.encodeWindows1251())
        val book = Fb2Format.parse(zip, "legacy.fb2.zip")

        assertEquals("Тестовая книга", book.title)
        assertEquals("Привет, мир.", book.sections.single().paragraphs.single())
    }

    @Test
    fun parsesWindows1252PlainXml() {
        val book = Fb2Format.parse(windows1252Xml.encodeWindows1252(), "legacy.fb2")

        assertEquals("The “Test” Book", book.title)
        assertEquals("It’s 25€ for café — OK.", book.sections.single().paragraphs.single())
    }

    @Test
    fun parsesWindows1252StoredZip() {
        val zip = Fb2Zip.createStoredZip("legacy.fb2", windows1252Xml.encodeWindows1252())
        val book = Fb2Format.parse(zip, "legacy.fb2.zip")

        assertEquals("The “Test” Book", book.title)
        assertEquals("It’s 25€ for café — OK.", book.sections.single().paragraphs.single())
    }

    @Test
    fun fallsBackToUtf8ForUnknownEncoding() {
        val xml = sampleXml.replace("encoding=\"UTF-8\"", "encoding=\"KOI8-R\"")
        val book = Fb2Format.parse(xml.encodeToByteArray(), "unknown.fb2")

        assertEquals("The Test Book", book.title)
    }

    @Test
    fun preservesReadableBlocksAndInlineStyles() {
        val book = Fb2Format.parseXml(richXml)
        val section = book.sections.single()

        assertEquals("Part", section.blocks[0].let { (it as Fb2Block.Subtitle).content.plainText })
        val paragraph = section.blocks[1] as Fb2Block.Paragraph
        assertEquals("Plain styled tail.", paragraph.content.plainText)
        assertEquals(setOf(Fb2TextStyle.Emphasis), paragraph.content.spans[1].styles)
        assertTrue(section.blocks[2] is Fb2Block.EmptyLine)
        assertEquals("pic.png", (section.blocks[3] as Fb2Block.Image).image.binaryId)
    }

    @Test
    fun parsesPoemsWithStanzasVersesAndInlineStyles() {
        val book = Fb2Format.parseXml(poemXml)
        val poem = (book.sections.single().blocks.single() as Fb2Block.Poem).poem

        assertEquals("Song", poem.title.single().plainText)
        val stanza = (poem.blocks.single() as Fb2PoemBlock.Stanza).stanza
        assertEquals("First stanza", stanza.title.single().plainText)
        assertEquals("Softly", stanza.subtitle?.plainText)
        assertEquals(listOf("Line one", "Line two."), stanza.verses.map { it.plainText })
        assertEquals(setOf(Fb2TextStyle.Emphasis), stanza.verses[0].spans.single().styles)
        assertEquals("Poet", poem.textAuthors.single().plainText)
        assertEquals("1910", poem.date)
    }

    @Test
    fun parsesBodySectionAndPoemEpigraphs() {
        val book = Fb2Format.parseXml(epigraphXml)

        assertEquals("Body quote", (book.bodyEpigraphs.single().blocks.single() as Fb2EpigraphBlock.Paragraph).content.plainText)

        val sectionBlocks = book.sections.single().blocks
        val sectionEpigraph = (sectionBlocks[0] as Fb2Block.Epigraph).epigraph
        assertEquals("Section quote", (sectionEpigraph.blocks[0] as Fb2EpigraphBlock.Paragraph).content.plainText)
        assertEquals("Author", sectionEpigraph.textAuthors.single().plainText)

        val cite = (sectionEpigraph.blocks[1] as Fb2EpigraphBlock.Cite).cite
        assertEquals("Cited line", (cite.blocks.single() as Fb2EpigraphBlock.Paragraph).content.plainText)
        assertEquals("Cited author", cite.textAuthors.single().plainText)

        val poem = (sectionBlocks[1] as Fb2Block.Poem).poem
        val poemEpigraph = poem.epigraphs.single()
        assertEquals("Poem quote", (poemEpigraph.blocks.single() as Fb2EpigraphBlock.Paragraph).content.plainText)
    }

    @Test
    fun parsesNoteLinksAndNotesBody() {
        val book = Fb2Format.parseXml(notesXml)
        val paragraph = book.sections.single().blocks.single() as Fb2Block.Paragraph
        val link = paragraph.content.spans.single { it.href != null }

        assertEquals("[1]", link.text)
        assertEquals("#n_1", link.href)
        assertEquals("note", link.linkType)
        assertEquals("Note text.", (book.notes.getValue("n_1").blocks.single() as Fb2Block.Paragraph).content.plainText)
    }

    private val sampleXml = """
        <?xml version="1.0" encoding="UTF-8"?>
        <FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:xlink="http://www.w3.org/1999/xlink">
          <description>
            <title-info>
              <genre>sf</genre>
              <author><first-name>Ada</first-name><last-name>Lovelace</last-name></author>
              <book-title>The Test Book</book-title>
              <lang>en</lang>
            </title-info>
            <document-info>
              <author><nickname>Toread</nickname></author>
              <date>2026-05-12</date>
              <id>sample</id>
              <version>1.0</version>
            </document-info>
          </description>
          <body>
            <section>
              <title><p>Chapter 1</p></title>
              <p>Hello &amp; welcome.</p>
            </section>
          </body>
        </FictionBook>
    """.trimIndent()

    private val richXml = """
        <?xml version="1.0" encoding="UTF-8"?>
        <FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:xlink="http://www.w3.org/1999/xlink">
          <description>
            <title-info>
              <author><nickname>A</nickname></author>
              <book-title>Rich</book-title>
              <lang>en</lang>
            </title-info>
            <document-info>
              <author><nickname>Toread</nickname></author>
              <date>2026-05-12</date>
              <id>rich</id>
              <version>1.0</version>
            </document-info>
          </description>
          <body>
            <section>
              <subtitle>Part</subtitle>
              <p>Plain <emphasis>styled</emphasis> tail.</p>
              <empty-line/>
              <image xlink:href="#pic.png"/>
            </section>
          </body>
        </FictionBook>
    """.trimIndent()

    private val poemXml = """
        <?xml version="1.0" encoding="UTF-8"?>
        <FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:xlink="http://www.w3.org/1999/xlink">
          <description>
            <title-info>
              <author><nickname>A</nickname></author>
              <book-title>Poetry</book-title>
              <lang>en</lang>
            </title-info>
            <document-info>
              <author><nickname>Toread</nickname></author>
              <date>2026-05-12</date>
              <id>poetry</id>
              <version>1.0</version>
            </document-info>
          </description>
          <body>
            <section>
              <poem>
                <title><p>Song</p></title>
                <stanza>
                  <title><p>First stanza</p></title>
                  <subtitle>Softly</subtitle>
                  <v>
                    <emphasis>Line one</emphasis>
                  </v>
                  <v>Line two.</v>
                </stanza>
                <text-author>Poet</text-author>
                <date>1910</date>
              </poem>
            </section>
          </body>
        </FictionBook>
    """.trimIndent()

    private val epigraphXml = """
        <?xml version="1.0" encoding="UTF-8"?>
        <FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:xlink="http://www.w3.org/1999/xlink">
          <description>
            <title-info>
              <author><nickname>A</nickname></author>
              <book-title>Epigraphs</book-title>
              <lang>en</lang>
            </title-info>
            <document-info>
              <author><nickname>Toread</nickname></author>
              <date>2026-05-12</date>
              <id>epigraphs</id>
              <version>1.0</version>
            </document-info>
          </description>
          <body>
            <epigraph><p>Body quote</p></epigraph>
            <section>
              <epigraph>
                <p>Section quote</p>
                <cite>
                  <p>Cited line</p>
                  <text-author>Cited author</text-author>
                </cite>
                <text-author>Author</text-author>
              </epigraph>
              <poem>
                <epigraph><p>Poem quote</p></epigraph>
                <stanza><v>Line.</v></stanza>
              </poem>
            </section>
          </body>
        </FictionBook>
    """.trimIndent()

    private val windows1251Xml = """
        <?xml version="1.0" encoding="windows-1251"?>
        <FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:xlink="http://www.w3.org/1999/xlink">
          <description>
            <title-info>
              <author><nickname>Автор</nickname></author>
              <book-title>Тестовая книга</book-title>
              <lang>ru</lang>
            </title-info>
            <document-info>
              <author><nickname>Toread</nickname></author>
              <date>2026-05-12</date>
              <id>legacy</id>
              <version>1.0</version>
            </document-info>
          </description>
          <body><section><p>Привет, мир.</p></section></body>
        </FictionBook>
    """.trimIndent()

    private val windows1252Xml = """
        <?xml version="1.0" encoding="windows-1252"?>
        <FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:xlink="http://www.w3.org/1999/xlink">
          <description>
            <title-info>
              <author><nickname>Author</nickname></author>
              <book-title>The “Test” Book</book-title>
              <lang>en</lang>
            </title-info>
            <document-info>
              <author><nickname>Toread</nickname></author>
              <date>2026-05-12</date>
              <id>legacy-1252</id>
              <version>1.0</version>
            </document-info>
          </description>
          <body><section><p>It’s 25€ for café — OK.</p></section></body>
        </FictionBook>
    """.trimIndent()

    private val notesXml = """
        <?xml version="1.0" encoding="UTF-8"?>
        <FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:l="http://www.w3.org/1999/xlink">
          <description>
            <title-info>
              <author><nickname>A</nickname></author>
              <book-title>Notes</book-title>
              <lang>en</lang>
            </title-info>
            <document-info>
              <author><nickname>Toread</nickname></author>
              <date>2026-05-12</date>
              <id>notes</id>
              <version>1.0</version>
            </document-info>
          </description>
          <body>
            <section>
              <p>Main<a l:href="#n_1" type="note">[1]</a>.</p>
            </section>
          </body>
          <body name="notes">
            <section id="n_1">
              <p>Note text.</p>
            </section>
          </body>
        </FictionBook>
    """.trimIndent()

    private fun String.encodeWindows1251(): ByteArray =
        ByteArray(length) { index ->
            val char = this[index]
            val value = when {
                char.code <= 0x7f -> char.code
                char in 'А'..'я' -> 0xc0 + char.code - 'А'.code
                char == 'Ё' -> 0xa8
                char == 'ё' -> 0xb8
                else -> error("Test character $char is not mapped to windows-1251")
            }
            value.toByte()
        }

    private fun String.encodeWindows1252(): ByteArray =
        ByteArray(length) { index ->
            val char = this[index]
            val value = when {
                char.code <= 0x7f -> char.code
                char.code in 0xa0..0xff -> char.code
                char == '€' -> 0x80
                char == '‘' -> 0x91
                char == '’' -> 0x92
                char == '“' -> 0x93
                char == '”' -> 0x94
                char == '–' -> 0x96
                char == '—' -> 0x97
                char == '™' -> 0x99
                else -> error("Test character $char is not mapped to windows-1252")
            }
            value.toByte()
        }
}
