package ru.forbion.f7cloud.core.network

import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * Тесты round-trip редактора vCard (P1 правка контакта, mail/072).
 * Ключевое: неизвестные свойства (PHOTO, X-*, UID) сохраняются, управляемые — заменяются.
 * Краевые из письма лида: FN с запятыми, PHOTO base64, многострочный ADR, CRLF-фолдинг.
 */
class VCardEditorTest {

    private fun edit(raw: String, fields: VCardFields) = VCardEditor.applyEdits(raw, fields)

    private val base = VCardFields(displayName = "Иван Петров")

    @Test fun preserves_unknown_properties_and_uid_and_photo() {
        val raw = buildString {
            append("BEGIN:VCARD\r\n")
            append("VERSION:3.0\r\n")
            append("UID:abc-123\r\n")
            append("FN:Старое Имя\r\n")
            append("N:Имя;Старое;;;\r\n")
            append("PHOTO;ENCODING=b;TYPE=JPEG:/9j/4AAQSkZJRgABAQAAAQABAAD\r\n")
            append("X-CUSTOM-FIELD:держим-как-есть\r\n")
            append("TEL;TYPE=CELL:+70000000000\r\n")
            append("END:VCARD\r\n")
        }
        val out = edit(
            raw,
            VCardFields(
                displayName = "Иван Петров",
                phones = listOf(VCardTypedValue("+79990001122", "WORK")),
            ),
        )
        // UID и неизвестные свойства сохранены
        assertTrue("UID сохранён", out.contains("UID:abc-123"))
        assertTrue("PHOTO сохранён", out.contains("/9j/4AAQSkZJRgABAQAAAQABAAD"))
        assertTrue("X-CUSTOM сохранён", out.contains("X-CUSTOM-FIELD:держим-как-есть"))
        // управляемые поля заменены на новые
        assertTrue("новый FN", out.contains("FN:Иван Петров"))
        assertFalse("старый FN убран", out.contains("Старое Имя"))
        assertTrue("новый телефон", out.contains("TEL;TYPE=WORK:+79990001122"))
        assertFalse("старый телефон убран", out.contains("+70000000000"))
        // валидный каркас
        assertTrue(out.startsWith("BEGIN:VCARD\r\n"))
        assertTrue(out.trimEnd().endsWith("END:VCARD"))
        assertEquals("одна VERSION", 1, Regex("(?m)^VERSION:").findAll(out).count())
    }

    @Test fun fn_with_commas_is_kept_literally() {
        val raw = "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:x\r\nFN:X\r\nEND:VCARD\r\n"
        val out = edit(raw, VCardFields(displayName = "Петров, Иван, Иванович"))
        assertTrue("запятые в FN сохранены литералом", out.contains("FN:Петров, Иван, Иванович"))
    }

    @Test fun folded_multiline_adr_is_regrouped_and_replaced() {
        // ADR разбит фолдингом на 2 физические строки (продолжение с пробела)
        val raw = buildString {
            append("BEGIN:VCARD\r\n")
            append("VERSION:3.0\r\n")
            append("UID:y\r\n")
            append("FN:Кто-то\r\n")
            append("ADR;TYPE=HOME:;;ул. Очень Длинная Дом 1 Корпус 2 Строение 3\r\n")
            append(" Квартира 456 Подъезд 7;Город;;;\r\n")
            append("END:VCARD\r\n")
        }
        val out = edit(raw, VCardFields(displayName = "Кто-то", address = "Новый Город, ул. Новая 5"))
        assertTrue("новый адрес в ADR", out.contains("Новый Город"))
        assertFalse("старый адрес убран", out.contains("Очень Длинная"))
        // не осталось «висящей» строки-продолжения старого ADR
        assertFalse(out.contains("Квартира 456"))
    }

    @Test fun long_line_is_folded_at_75_with_crlf_space() {
        val longNote = "Z".repeat(200)
        val raw = "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:z\r\nFN:N\r\nEND:VCARD\r\n"
        val out = edit(raw, base.copy(note = longNote))
        // каждая физическая строка ≤ 76 символов (75 + возможный ведущий пробел)
        val physical = out.split("\r\n").filter { it.isNotEmpty() }
        assertTrue("все строки свёрнуты ≤76", physical.all { it.length <= 76 })
        // продолжения начинаются с пробела
        assertTrue("есть свёрнутое продолжение", physical.any { it.startsWith(" Z") })
        // при развороте NOTE восстанавливается целиком
        val unfolded = out.replace("\r\n ", "")
        assertTrue(unfolded.contains("NOTE:$longNote"))
    }

    @Test fun crlf_and_lf_inputs_both_parse() {
        val lf = "BEGIN:VCARD\nVERSION:3.0\nUID:m\nFN:Старое\nX-KEEP:1\nEND:VCARD\n"
        val out = edit(lf, base)
        assertTrue(out.contains("X-KEEP:1"))
        assertTrue(out.contains("FN:Иван Петров"))
        // выход всегда CRLF
        assertTrue(out.contains("\r\n"))
        assertFalse("нет одиночных LF", out.replace("\r\n", "").contains("\n"))
    }

    @Test fun n_derived_from_fn_when_name_parts_absent() {
        val raw = "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:n\r\nEND:VCARD\r\n"
        val out = edit(raw, VCardFields(displayName = "Иван Петров"))
        // N: фамилия;имя;;; — из FN: given=Иван, family=Петров
        assertTrue(out.contains("N:Петров;Иван;;;"))
    }

    @Test fun explicit_name_parts_win_over_fn() {
        val raw = "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:e\r\nEND:VCARD\r\n"
        val out = edit(raw, VCardFields(displayName = "Ivan P", firstName = "Ivan", lastName = "Petrov"))
        assertTrue(out.contains("N:Petrov;Ivan;;;"))
    }

    @Test fun multiple_emails_and_types_emitted() {
        val raw = "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:me\r\nFN:X\r\nEMAIL:old@x.ru\r\nEND:VCARD\r\n"
        val out = edit(
            raw,
            base.copy(
                emails = listOf(
                    VCardTypedValue("home@x.ru", "HOME"),
                    VCardTypedValue("work@x.ru", "WORK"),
                ),
            ),
        )
        assertFalse(out.contains("old@x.ru"))
        assertTrue(out.contains("EMAIL;TYPE=HOME:home@x.ru"))
        assertTrue(out.contains("EMAIL;TYPE=WORK:work@x.ru"))
    }

    @Test fun blank_managed_fields_are_omitted() {
        val raw = "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:b\r\nFN:X\r\nEND:VCARD\r\n"
        val out = edit(raw, VCardFields(displayName = "Только Имя"))
        assertFalse("нет пустого EMAIL", out.contains("EMAIL"))
        assertFalse("нет пустого TEL", out.contains("TEL"))
        assertFalse("нет пустого ADR", out.contains("ADR"))
        assertFalse("нет пустого ORG", out.contains("ORG"))
    }

    // --- P2: разбор типов TEL/EMAIL ---

    @Test fun parses_email_and_phone_types() {
        val raw = buildString {
            append("BEGIN:VCARD\r\nVERSION:3.0\r\nUID:t\r\nFN:X\r\n")
            append("EMAIL;TYPE=WORK:work@x.ru\r\n")
            append("EMAIL;TYPE=HOME:home@x.ru\r\n")
            append("TEL;TYPE=CELL:+7900\r\n")
            append("TEL;TYPE=WORK:+7495\r\n")
            append("END:VCARD\r\n")
        }
        val emails = VCardEditor.parseTypedValues(raw, "EMAIL")
        assertEquals(listOf("work@x.ru", "home@x.ru"), emails.map { it.value })
        assertEquals(listOf("WORK", "HOME"), emails.map { it.type })
        val phones = VCardEditor.parseTypedValues(raw, "TEL")
        assertEquals(listOf("CELL", "WORK"), phones.map { it.type })
    }

    @Test fun ignores_technical_type_tokens() {
        // INTERNET/VOICE/PREF — технические, берём содержательный токен
        val raw = "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:i\r\nFN:X\r\n" +
            "EMAIL;TYPE=INTERNET,HOME:h@x.ru\r\n" +
            "TEL;TYPE=VOICE,CELL:+7900\r\n" +
            "END:VCARD\r\n"
        assertEquals("HOME", VCardEditor.parseTypedValues(raw, "EMAIL").single().type)
        assertEquals("CELL", VCardEditor.parseTypedValues(raw, "TEL").single().type)
    }

    @Test fun typeless_value_yields_blank_type() {
        val raw = "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:n\r\nFN:X\r\nEMAIL:plain@x.ru\r\nEND:VCARD\r\n"
        val e = VCardEditor.parseTypedValues(raw, "EMAIL").single()
        assertEquals("plain@x.ru", e.value)
        assertEquals("", e.type)
    }

    @Test fun round_trip_preserves_parsed_types() {
        val raw = "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:r\r\nFN:X\r\n" +
            "EMAIL;TYPE=WORK:w@x.ru\r\nTEL;TYPE=HOME:+7495\r\nEND:VCARD\r\n"
        val emails = VCardEditor.parseTypedValues(raw, "EMAIL")
        val phones = VCardEditor.parseTypedValues(raw, "TEL")
        val out = VCardEditor.applyEdits(raw, VCardFields(displayName = "X", emails = emails, phones = phones))
        // типы не потеряны при правке (чиним P1-каветат нормализации)
        assertTrue(out.contains("EMAIL;TYPE=WORK:w@x.ru"))
        assertTrue(out.contains("TEL;TYPE=HOME:+7495"))
    }
}
