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

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

/**
 * Decoding tests for the self-service ("me") account DTOs (PLAN.md §6.3, §6.4).
 * Shapes come from the website's `account.controller` handlers surfaced under
 * `/auth/me/account*`; unknown keys are ignored (additive fields, §8).
 */
class AccountDtoTest {

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

    @Test fun accountDecodesSecurityFlags() {
        val dto = json.decodeFromString<PlayerAccountDto>(
            """{"id":42,"username":"newplayer","role":"player","email":"p@example.com",
               "status":"active","totp_enabled":false,"has_password":true}""",
        )
        assertEquals(42L, dto.id)
        assertEquals("newplayer", dto.username)
        assertTrue(dto.has_password)
        assertFalse(dto.totp_enabled)
    }

    @Test fun ssoAccountHasNoPassword() {
        // An SSO-provisioned account omits email and reports has_password=false.
        val dto = json.decodeFromString<PlayerAccountDto>(
            """{"id":7,"username":"ssouser","role":"player","email":null,
               "status":"active","totp_enabled":true,"has_password":false}""",
        )
        assertFalse(dto.has_password)
        assertTrue(dto.totp_enabled)
    }

    @Test fun totpSetupDecodesQr() {
        val dto = json.decodeFromString<TotpSetupDto>(
            """{"otpauthUrl":"otpauth://totp/Runic:admin?secret=ABC","qr":"data:image/png;base64,iVBORw0KGgo="}""",
        )
        assertTrue(dto.qr!!.startsWith("data:image/png;base64,"))
    }

    @Test fun totpStateDecodes() {
        assertTrue(json.decodeFromString<TotpStateDto>("""{"totp_enabled":true}""").totp_enabled)
    }

    @Test fun totpEnableCarriesOneTimeRecoveryCodes() {
        // Enabling 2FA now returns the fresh single-use batch once (TRUSTED_DEVICES_MFA.md).
        val dto = json.decodeFromString<TotpStateDto>(
            """{"totp_enabled":true,"recoveryCodes":["aaaa-1111","bbbb-2222"]}""",
        )
        assertTrue(dto.totp_enabled)
        assertEquals(listOf("aaaa-1111", "bbbb-2222"), dto.recoveryCodes)
    }

    @Test fun totpStateDisableHasNoRecoveryCodes() {
        // Disable (and older backends) omit the field — must decode to null, not crash.
        val dto = json.decodeFromString<TotpStateDto>("""{"totp_enabled":false}""")
        assertFalse(dto.totp_enabled)
        assertEquals(null, dto.recoveryCodes)
    }

    @Test fun trustedDeviceDecodes() {
        val dto = json.decodeFromString<TrustedDeviceDto>(
            """{"id":5,"platform":"mobile","deviceName":"Pixel 8","userAgent":"RunicGatewayApp/1.0",
               "createdAt":"2026-07-20T10:00:00Z","lastUsedAt":"2026-07-22T09:00:00Z",
               "expiresAt":"2026-08-19T10:00:00Z"}""",
        )
        assertEquals(5L, dto.id)
        assertEquals("mobile", dto.platform)
        assertEquals("Pixel 8", dto.deviceName)
        assertEquals("2026-07-22T09:00:00Z", dto.lastUsedAt)
    }

    @Test fun trustDeviceResultCarriesNativeToken() {
        val dto = json.decodeFromString<TrustDeviceResultDto>(
            """{"trusted":true,"trustToken":"opaque-token-abc"}""",
        )
        assertTrue(dto.trusted)
        assertEquals("opaque-token-abc", dto.trustToken)
    }

    @Test fun trustedDeviceLimitDecodesDevices() {
        val dto = json.decodeFromString<TrustedDeviceLimitDto>(
            """{"error":"trusted_device_limit","devices":[
                 {"id":1,"platform":"web","deviceName":"Firefox"},
                 {"id":2,"platform":"mobile","deviceName":"Pixel"}]}""",
        )
        assertEquals("trusted_device_limit", dto.error)
        assertEquals(2, dto.devices.size)
        assertEquals(2L, dto.devices[1].id)
    }

    @Test fun recoveryStatusAndCodesDecode() {
        assertEquals(7, json.decodeFromString<RecoveryStatusDto>("""{"remaining":7}""").remaining)
        val codes = json.decodeFromString<RecoveryCodesDto>(
            """{"recoveryCodes":["c1","c2","c3"]}""",
        )
        assertEquals(3, codes.recoveryCodes.size)
    }

    @Test fun revokedResultsDecode() {
        assertTrue(json.decodeFromString<RevokedFlagDto>("""{"revoked":true}""").revoked)
        assertEquals(4, json.decodeFromString<RevokedCountDto>("""{"revoked":4}""").revoked)
    }

    @Test fun linkedIdentityDecodes() {
        val dto = json.decodeFromString<LinkedIdentityDto>(
            """{"provider":"discord","email":"u@example.com","linked_at":"2026-07-19T22:00:00Z"}""",
        )
        assertEquals("discord", dto.provider)
        assertEquals("u@example.com", dto.email)
    }
}
