package controlplane

import (
	"bytes"
	"context"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"git.toki-labs.com/toki/gito/services/core/internal/config"
	"git.toki-labs.com/toki/gito/services/core/internal/protosocket"
	toki "git.toki-labs.com/toki/proto-socket/go"
	"google.golang.org/protobuf/types/known/structpb"
	"nhooyr.io/websocket"
)

func TestBinaryProtoSocketBranchUpdated(t *testing.T) {
	ctx := context.Background()

	srv := httptest.NewServer(newRouterWithStore(config.Config{
		AppEnv:          "test",
		ProtoSocketPath: "/proto-socket",
	}, slog.Default(), nil))
	t.Cleanup(srv.Close)

	// 1. Register branch watch via REST
	watchReq, _ := http.NewRequest(http.MethodPost, srv.URL+"/api/listeners/branches",
		bytes.NewBufferString(`{"repo_id":"wire-repo","branch":"main","provider":"forgejo"}`))
	watchReq.Header.Set("Content-Type", "application/json")
	watchResp, err := http.DefaultClient.Do(watchReq)
	if err != nil {
		t.Fatalf("register branch watch: %v", err)
	}
	watchResp.Body.Close()
	if watchResp.StatusCode != http.StatusCreated {
		t.Fatalf("register branch watch status: %d", watchResp.StatusCode)
	}

	// 2. Connect binary proto-socket client
	wsURL := strings.Replace(srv.URL, "http://", "ws://", 1) + "/proto-socket"
	conn, _, err := websocket.Dial(ctx, wsURL, nil)
	if err != nil {
		t.Fatalf("websocket dial: %v", err)
	}
	client := toki.NewWsClient(conn, 0, 0, protosocket.ParserMap())
	t.Cleanup(func() { client.Close() })

	// 3. Register listener BEFORE subscribe to avoid missing early broadcasts
	received := make(chan *structpb.Struct, 1)
	toki.AddListenerTyped[*structpb.Struct](&client.Communicator, func(m *structpb.Struct) {
		select {
		case received <- m:
		default:
		}
	})

	// 4. Send event.subscribe via binary proto-socket
	subEnv := protosocket.Envelope{
		ProtocolVersion: protosocket.ProtocolVersion,
		ID:              "sub-1",
		Type:            "request",
		Channel:         "event",
		Action:          "event.subscribe",
		Payload: map[string]any{
			"events":  []any{"branch.updated"},
			"repo_id": "wire-repo",
			"branch":  "main",
		},
	}
	subMsg, err := subEnv.ToStruct()
	if err != nil {
		t.Fatalf("marshal subscribe envelope: %v", err)
	}
	res, err := toki.SendRequestTyped[*structpb.Struct, *structpb.Struct](
		&client.Communicator,
		subMsg,
		5*time.Second,
	)
	if err != nil {
		t.Fatalf("event.subscribe: %v", err)
	}
	resEnv, err := protosocket.EnvelopeFromStruct(res)
	if err != nil {
		t.Fatalf("parse subscribe response: %v", err)
	}
	if resEnv.Type != "response" || resEnv.Action != "event.subscribe" {
		t.Fatalf("subscribe response: type=%q action=%q", resEnv.Type, resEnv.Action)
	}

	// 5. Trigger Forgejo push
	pushBody := `{"ref":"refs/heads/main","before":"sha-before","after":"sha-after",` +
		`"repository":{"name":"wire-repo","full_name":"owner/wire-repo"},` +
		`"commits":[{"id":"sha-after","modified":["docs/README.md"],"added":[],"removed":[]}]}`
	pushReq, _ := http.NewRequest(http.MethodPost,
		srv.URL+"/callbacks/forgejo/push?repo_id=wire-repo",
		bytes.NewBufferString(pushBody))
	pushReq.Header.Set("X-Forgejo-Event", "push")
	pushReq.Header.Set("X-Forgejo-Delivery", "wire-delivery-1")
	pushResp, err := http.DefaultClient.Do(pushReq)
	if err != nil {
		t.Fatalf("forgejo push: %v", err)
	}
	pushResp.Body.Close()
	if pushResp.StatusCode != http.StatusAccepted {
		t.Fatalf("forgejo push status: %d", pushResp.StatusCode)
	}

	// 6. Receive branch.updated on binary proto-socket path
	select {
	case msg := <-received:
		env, err := protosocket.EnvelopeFromStruct(msg)
		if err != nil {
			t.Fatalf("parse event: %v", err)
		}
		if env.Type != "event" || env.Action != "branch.updated" {
			t.Fatalf("event: type=%q action=%q", env.Type, env.Action)
		}
		if env.Payload["before"] != "sha-before" {
			t.Errorf("before: got %v", env.Payload["before"])
		}
		if env.Payload["after"] != "sha-after" {
			t.Errorf("after: got %v", env.Payload["after"])
		}
		files, _ := env.Payload["changed_files"].([]any)
		if len(files) == 0 {
			t.Fatal("expected changed_files, got none")
		}
		firstFile, ok := files[0].(map[string]any)
		if !ok {
			t.Fatalf("changed_files[0]: got %T", files[0])
		}
		if firstFile["path"] != "docs/README.md" {
			t.Errorf("changed_files[0].path: got %v", firstFile["path"])
		}
		if firstFile["change_type"] != "modified" {
			t.Errorf("changed_files[0].change_type: got %v", firstFile["change_type"])
		}
	case <-time.After(5 * time.Second):
		t.Fatal("timed out waiting for branch.updated on binary proto-socket path")
	}
}
