package fr.bglands.client.protocol;

import com.google.gson.JsonParser;
import org.junit.jupiter.api.Test;

import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

final class UiProtocolCodecTest {
    private final UiProtocolCodec codec = new UiProtocolCodec();

    @Test
    void encodesExactHelloContract() {
        byte[] payload = codec.encode(ClientHello.current("1.0.0"));
        var json = JsonParser.parseString(new String(payload, StandardCharsets.UTF_8)).getAsJsonObject();

        assertEquals("hello", json.get("type").getAsString());
        assertEquals(1, json.get("protocolMajor").getAsInt());
        assertEquals(0, json.get("protocolMinor").getAsInt());
        assertEquals("1.0.0", json.get("modVersion").getAsString());
        assertEquals("screen-v1", json.getAsJsonArray("capabilities").get(0).getAsString());
        assertEquals("form-v1", json.getAsJsonArray("capabilities").get(1).getAsString());
    }

    @Test
    void decodesOpenScreenAndAllControlFields() throws Exception {
        String instance = UUID.randomUUID().toString();
        String token = UUID.randomUUID().toString();
        String json = """
                {
                  "type":"open",
                  "protocolMajor":1,
                  "instanceId":"%s",
                  "revision":7,
                  "route":"bglands:clan/home",
                  "title":"Mon clan",
                  "content":"Bienvenue",
                  "accent":"green",
                  "columns":2,
                  "background":"frame_main",
                  "showExitAction":true,
                  "controls":[{
                    "id":"c0",
                    "label":"Membres",
                    "description":"Voir les membres",
                    "icon":"friends",
                    "token":"%s",
                    "x":84,
                    "y":134,
                    "width":132,
                    "height":34,
                    "color":"#53e86a"
                  }]
                }
                """.formatted(instance, token);

        OpenScreen open = assertInstanceOf(
                OpenScreen.class,
                codec.decodeServer(json.getBytes(StandardCharsets.UTF_8))
        );
        assertEquals(7, open.revision());
        assertEquals("bglands:clan/home", open.route());
        assertEquals(2, open.columns());
        assertEquals(1, open.controls().size());
        assertEquals("friends", open.controls().get(0).icon());
        assertEquals(84, open.controls().get(0).x());
        assertEquals(134, open.controls().get(0).y());
        assertEquals(132, open.controls().get(0).width());
        assertEquals(34, open.controls().get(0).height());
    }

    @Test
    void encodesActionWithReplayProtectionFields() {
        UiAction action = UiAction.create(
                UUID.randomUUID().toString(),
                "c0",
                UUID.randomUUID().toString()
        );
        String json = new String(codec.encode(action), StandardCharsets.UTF_8);

        assertTrue(json.contains("\"type\":\"action\""));
        assertTrue(json.contains(action.requestId()));
        assertTrue(json.contains(action.token()));
    }

    @Test
    void rejectsMalformedAndOversizedPayloads() {
        assertThrows(ProtocolException.class, () -> codec.decodeServer("not-json".getBytes(StandardCharsets.UTF_8)));
        assertThrows(
                ProtocolException.class,
                () -> codec.decodeServer(new byte[ProtocolConstants.MAX_PAYLOAD_BYTES + 1])
        );
    }

    @Test
    void rejectsInvalidTokens() {
        String json = """
                {"type":"open","protocolMajor":1,"instanceId":"not-a-uuid","revision":1,
                "route":"bglands:test","controls":[]}
                """;
        assertThrows(ProtocolException.class, () -> codec.decodeServer(json.getBytes(StandardCharsets.UTF_8)));
    }

    @Test
    void decodesBackendProbe() throws Exception {
        ServerProbe probe = assertInstanceOf(
                ServerProbe.class,
                codec.decodeServer("""
                        {"type":"probe","protocolMajor":1,"protocolMinor":0,"serverVersion":"paper-26.2"}
                        """.getBytes(StandardCharsets.UTF_8))
        );
        assertEquals("paper-26.2", probe.serverVersion());
    }

    @Test
    void respectsRejectedHelloAcknowledgement() throws Exception {
        HelloAck ack = assertInstanceOf(HelloAck.class, codec.decodeServer("""
                {"type":"hello_ack","protocolMajor":1,"protocolMinor":0,"serverVersion":"paper-26.2",
                "accepted":false,"reason":"client_version"}
                """.getBytes(StandardCharsets.UTF_8)));
        assertEquals(false, ack.accepted());
        assertEquals("client_version", ack.reason());
    }

    @Test
    void decodesFormAndEncodesSubmit() throws Exception {
        String instanceId = UUID.randomUUID().toString();
        String submitToken = UUID.randomUUID().toString();
        OpenForm form = assertInstanceOf(OpenForm.class, codec.decodeServer("""
                {"type":"open_form","protocolMajor":1,"instanceId":"%s","route":"bglands:clan/create",
                "title":"Créer un clan","instruction":"Choisis un nom","submitToken":"%s",
                "fields":[{"id":"f0","label":"Nom","placeholder":"Mon clan","maxLength":1024}]}
                """.formatted(instanceId, submitToken).getBytes(StandardCharsets.UTF_8)));

        assertEquals(1024, form.fields().get(0).maxLength());
        UiSubmit submit = UiSubmit.create(instanceId, submitToken, Map.of("f0", "BG Team"));
        var json = JsonParser.parseString(new String(codec.encode(submit), StandardCharsets.UTF_8)).getAsJsonObject();
        assertEquals("submit", json.get("type").getAsString());
        assertEquals("BG Team", json.getAsJsonObject("values").get("f0").getAsString());
    }

    @Test
    void decodesVersionedAuthoredSceneWithOnlyOpaqueControlReferences() throws Exception {
        String instance = UUID.randomUUID().toString();
        String token = UUID.randomUUID().toString();
        OpenScreen open = assertInstanceOf(OpenScreen.class, codec.decodeServer("""
                {
                  "type":"open","protocolMajor":1,"instanceId":"%s","revision":1,
                  "route":"bglands:clan/join","title":"fallback","content":"fallback",
                  "controls":[{"id":"c0","label":"Créer","token":"%s","icon":"friends"}],
                  "scene":{
                    "schemaVersion":1,
                    "canvas":{"width":400,"height":300,"backgroundAsset":"/assets/frame-full.png"},
                    "widgets":[
                      {"id":"logo","kind":"logo","x":156,"y":15,"width":180,"height":60,"z":2,
                       "asset":"/assets/logo.png","align":"left"},
                      {"id":"card","kind":"panel","x":98,"y":93,"width":288,"height":28,"z":3,
                       "asset":"/assets/buttons/button_white.png","label":"Bienvenue","binding":"Texte","color":"#9cff82","fontSize":6,"align":"center"},
                      {"id":"create","kind":"button","x":104,"y":129,"width":132,"height":34,"z":5,
                       "asset":"/assets/buttons/button_green.png","label":"Créer un clan","color":"#c7ffbe","fontSize":6,
                       "align":"center","controlId":"c0"}
                    ]
                  }
                }
                """.formatted(instance, token).getBytes(StandardCharsets.UTF_8)));

        assertEquals(1, open.scene().schemaVersion());
        assertEquals("/assets/frame-full.png", open.scene().canvas().backgroundAsset());
        assertEquals(List.of("logo", "card", "create"), open.scene().widgets().stream().map(UiSceneWidget::id).toList());
        assertEquals("c0", open.scene().widgets().get(2).controlId());
    }

    @Test
    void rejectsSceneCommandsRemoteAssetsAndUnboundButtons() {
        String base = """
                {"type":"open","protocolMajor":1,"instanceId":"%s","revision":1,"route":"bglands:test",
                 "controls":[{"id":"c0","label":"OK","token":"%s"}],
                 "scene":{"schemaVersion":1,"canvas":{"width":400,"height":300,"backgroundAsset":"/assets/frame-full.png"},
                 "widgets":[{"id":"ok","kind":"button","x":1,"y":1,"width":120,"height":32,"z":1,
                 "asset":"/assets/buttons/button_green.png","controlId":"c0"%s}]}}
                """;
        String instance = UUID.randomUUID().toString();
        String token = UUID.randomUUID().toString();
        assertThrows(ProtocolException.class, () -> codec.decodeServer(
                base.formatted(instance, token, ",\"command\":\"/op attacker\"").getBytes(StandardCharsets.UTF_8)));
        assertThrows(ProtocolException.class, () -> codec.decodeServer(
                base.formatted(instance, token, "").replace("/assets/buttons/button_green.png", "https://evil.invalid/a.png")
                        .getBytes(StandardCharsets.UTF_8)));
        assertThrows(ProtocolException.class, () -> codec.decodeServer(
                base.formatted(instance, token, "").replace("\"controlId\":\"c0\"", "\"controlId\":\"c1\"")
                        .getBytes(StandardCharsets.UTF_8)));
    }
}
