/*
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package com.runicgateway.app.data.api.dto

import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * Decoding tests for the opt-in push DTOs (PLAN.md §11, M7 Part 2). Shapes come
 * from the merged backend (`notifications.controller` / `pushDevices.model`);
 * unknown keys are ignored (additive fields, §8).
 */
class NotificationsDtoTest {

    private val json = Json {
        ignoreUnknownKeys = true
        explicitNulls = false
        coerceInputValues = true
    }

    @Test fun pushDeviceDecodes() {
        val dto = json.decodeFromString<PushDeviceDto>(
            """{"id":9,"transport":"unifiedpush","endpoint":"https://ntfy.example.com/up123",
               "platform":"android","createdAt":"2026-07-20T00:00:00Z","lastSeenAt":null}""",
        )
        assertEquals(9L, dto.id)
        assertEquals("unifiedpush", dto.transport)
        assertEquals("https://ntfy.example.com/up123", dto.endpoint)
        assertEquals("android", dto.platform)
        assertNull(dto.lastSeenAt)
    }

    @Test fun streamCatalogDecodesPersonalFlags() {
        val dto = json.decodeFromString<NotificationStreamsDto>(
            """{"streams":[
                 {"id":"news.post","label":"News posts","description":"New posts.","personal":false,"requiresLinkedAccount":false},
                 {"id":"vendor.sale","label":"Your vendor sold","description":"A sale.","personal":true,"requiresLinkedAccount":true}
               ]}""",
        )
        assertEquals(2, dto.streams.size)
        val news = dto.streams.first { it.id == "news.post" }
        assertFalse(news.personal)
        assertFalse(news.requiresLinkedAccount)
        val vendor = dto.streams.first { it.id == "vendor.sale" }
        assertTrue(vendor.personal)
        assertTrue(vendor.requiresLinkedAccount)
    }

    @Test fun subscriptionsDecode() {
        val dto = json.decodeFromString<NotificationSubscriptionsDto>(
            """{"streams":["news.post","champ.start"]}""",
        )
        assertEquals(listOf("news.post", "champ.start"), dto.streams)
    }

    @Test fun emptySubscriptionsStillSerializeStreamsField() {
        // Regression: clearing the LAST subscription sends an empty set. The backend
        // validator requires `streams`, so it must be present as `[]`, not omitted.
        // Uses the production Json config (no encodeDefaults) to prove the field is
        // always emitted because the DTO field has no default.
        val body = json.encodeToString(NotificationSubscriptionsDto(emptyList()))
        assertEquals("""{"streams":[]}""", body)
    }

    @Test fun settingsPushBlockDecodes() {
        val dto = json.decodeFromString<SettingsDto>(
            """{"site_title":"Shard","brand":{"name":"Shard"},"push":{"ntfyUrl":"https://ntfy.shard.tld"}}""",
        )
        assertEquals("https://ntfy.shard.tld", dto.push.ntfyUrl)
    }

    @Test fun settingsPushDefaultsNullOnOlderBackend() {
        // A backend predating M7 omits `push` entirely — the app must still decode.
        val dto = json.decodeFromString<SettingsDto>(
            """{"site_title":"Shard","brand":{"name":"Shard"}}""",
        )
        assertNull(dto.push.ntfyUrl)
    }
}
