/*
 * 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.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * Decoding tests for the mobile auth wire shapes (PLAN.md §4.1). Field names come
 * from the website's `auth/mobile` controller + `/auth/me`; the parser ignores
 * unknown keys (additive backend fields, §8).
 */
class AuthDtoTest {

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

    @Test fun tokenResponseDecodesWithSafeUser() {
        val dto = json.decodeFromString<MobileTokenResponse>(
            """{"accessToken":"aaa.bbb.ccc","refreshToken":"r-123","expiresIn":"15m",
               "user":{"id":7,"username":"alice","role":"player"}}""",
        )
        assertEquals("aaa.bbb.ccc", dto.accessToken)
        assertEquals("r-123", dto.refreshToken)
        assertEquals("15m", dto.expiresIn)
        assertEquals(7L, dto.user.id)
        assertEquals("alice", dto.user.username)
        assertEquals("player", dto.user.role)
    }

    @Test fun meResponseIgnoresExtraUserFields() {
        // /auth/me returns the full safe user row — the app only needs id/username/role.
        val dto = json.decodeFromString<MeResponse>(
            """{"user":{"id":1,"username":"staff","role":"editor","status":"active",
               "createdAt":"2026-01-01T00:00:00Z","totp_enabled":true}}""",
        )
        assertEquals(1L, dto.user.id)
        assertEquals("editor", dto.user.role)
    }

    @Test fun totpRequiredErrorDecodes() {
        val dto = json.decodeFromString<TotpRequiredError>(
            """{"totpRequired":true,"message":"A verification code is required."}""",
        )
        assertTrue(dto.totpRequired)
    }

    @Test fun totpRequiredDefaultsFalseForPlainCredentialFailure() {
        // A bad-password 401 has no totpRequired flag — must not read as a 2FA prompt.
        val dto = json.decodeFromString<TotpRequiredError>(
            """{"message":"Incorrect username or password."}""",
        )
        assertFalse(dto.totpRequired)
    }

    @Test fun tokenResponseExpiresInOptional() {
        val dto = json.decodeFromString<MobileTokenResponse>(
            """{"accessToken":"a","refreshToken":"r","user":{"id":2,"username":"bob","role":"admin"}}""",
        )
        assertNull(dto.expiresIn)
        assertEquals("admin", dto.user.role)
    }

    @Test fun loginCarriesTrustTokenWhenDeviceTrusted() {
        // trustDevice accepted → an opaque token to persist + replay (TRUSTED_DEVICES_MFA.md).
        val dto = json.decodeFromString<MobileTokenResponse>(
            """{"accessToken":"a","refreshToken":"r","user":{"id":3,"username":"c","role":"player"},
               "trustToken":"opaque-abc"}""",
        )
        assertEquals("opaque-abc", dto.trustToken)
        assertFalse(dto.trustLimitReached)
    }

    @Test fun loginSignalsTrustLimitWithDevices() {
        // At the cap: login still succeeds, but no token; the device list is returned.
        val dto = json.decodeFromString<MobileTokenResponse>(
            """{"accessToken":"a","refreshToken":"r","user":{"id":3,"username":"c","role":"player"},
               "trustLimitReached":true,"devices":[{"id":1,"platform":"mobile","deviceName":"Old"}]}""",
        )
        assertNull(dto.trustToken)
        assertTrue(dto.trustLimitReached)
        assertEquals(1, dto.devices.size)
    }

    @Test fun loginWithoutTrustFieldsDefaultsCleanly() {
        // A normal (no-trust) login omits every trust field — must not crash or mis-flag.
        val dto = json.decodeFromString<MobileTokenResponse>(
            """{"accessToken":"a","refreshToken":"r","user":{"id":4,"username":"d","role":"player"}}""",
        )
        assertNull(dto.trustToken)
        assertFalse(dto.trustLimitReached)
        assertTrue(dto.devices.isEmpty())
    }
}
