package bridge

import (
	"bytes"
	"encoding/json"
	"net/netip"
	"strings"
	"testing"

	"tailscale.com/ipn"
	"tailscale.com/ipn/ipnstate"
	"tailscale.com/tailcfg"
	"tailscale.com/types/netmap"
)

func TestWriteEventDelimitedJSON(t *testing.T) {
	t.Parallel()

	var buf bytes.Buffer
	if err := writeEvent(&buf, Event{
		Type:       BridgeEventTypeState,
		State:      ipn.Running.String(),
		StateLabel: stateSummary(ipn.Running.String()),
	}); err != nil {
		t.Fatalf("writeEvent(state): %v", err)
	}
	if err := writeEvent(&buf, Event{
		Type:  BridgeEventTypeBackendUnavailable,
		Error: "dial unix /var/run/tailscale/tailscaled.sock: connect: no such file or directory",
	}); err != nil {
		t.Fatalf("writeEvent(backend-unavailable): %v", err)
	}

	lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
	if len(lines) != 2 {
		t.Fatalf("expected 2 JSON lines, got %d: %q", len(lines), buf.String())
	}

	var first map[string]any
	if err := json.Unmarshal([]byte(lines[0]), &first); err != nil {
		t.Fatalf("unmarshal first event: %v", err)
	}
	if got, want := first["type"], BridgeEventTypeState; got != want {
		t.Fatalf("first event type = %v, want %q", got, want)
	}
	if got, want := first["state"], ipn.Running.String(); got != want {
		t.Fatalf("first event state = %v, want %q", got, want)
	}
}

func TestWatchStateApplyNotifyEmitsTypedEvents(t *testing.T) {
	t.Parallel()

	initialPrefs := &ipn.Prefs{WantRunning: false, CorpDNS: false, RouteAll: false}
	updatedPrefs := &ipn.Prefs{WantRunning: true, CorpDNS: true, RouteAll: true}
	updatedPrefsView := updatedPrefs.View()
	errText := "login failed"
	nextState := ipn.Running

	state := newWatchState(&snapshot{
		status: &ipnstate.Status{
			BackendState: ipn.NeedsLogin.String(),
			Version:      "1.0.0-test",
		},
		prefs:              initialPrefs,
		netmap:             newTestNetworkMap("alpha"),
		controllerInput:    "https://sdp.mm.md",
		hasControllerStore: true,
		controlURL:         "https://sdp.mm.md",
	})

	events := state.applyNotify(ipn.Notify{
		Version:     "1.1.0-test",
		ErrMessage:  &errText,
		BrowseToURL: ptrTo("https://login.example.invalid/flow"),
		State:       &nextState,
		Prefs:       &updatedPrefsView,
		NetMap:      newTestNetworkMap("beta"),
	})

	if got, want := eventTypes(events), []string{
		BridgeEventTypeVersion,
		BridgeEventTypeError,
		BridgeEventTypeBrowseURL,
		BridgeEventTypeState,
		BridgeEventTypePrefs,
		BridgeEventTypeNetmap,
	}; !equalStrings(got, want) {
		t.Fatalf("event types = %v, want %v", got, want)
	}
	if got, want := events[3].StateLabel, stateSummary(ipn.Running.String()); got != want {
		t.Fatalf("state label = %q, want %q", got, want)
	}
	if events[4].Prefs == nil || !events[4].Prefs.WantRunning || !events[4].Prefs.CorpDNS || !events[4].Prefs.RouteAll {
		t.Fatalf("prefs payload = %#v, want updated prefs summary", events[4].Prefs)
	}
	if events[5].Netmap == nil || events[5].Netmap.Self.DNSName != "beta.tailnet.ts.net" {
		t.Fatalf("netmap payload = %#v", events[5].Netmap)
	}
}

func TestBuildGUISnapshotIncludesAutoStart(t *testing.T) {
	t.Parallel()

	snap := &snapshot{
		status: &ipnstate.Status{
			BackendState: ipn.Stopped.String(),
			Version:      "1.2.3",
			AuthURL:      "https://sdp.mm.md/a/login-flow",
		},
		prefs: &ipn.Prefs{
			WantRunning: false,
		},
		netmap:             newTestNetworkMap("gamma"),
		controllerInput:    "https://sdp.mm.md",
		hasControllerStore: true,
		controlURL:         "https://sdp.mm.md",
		autoStartEnabled:   true,
	}

	gui := buildGUISnapshot(snap)
	if !gui.Prefs.AutoStartUp {
		t.Fatal("Prefs.AutoStartUp = false, want true")
	}
	if got, want := gui.AuthURL, "https://sdp.mm.md/a/login-flow"; got != want {
		t.Fatalf("AuthURL = %q, want %q", got, want)
	}
}

func newTestNetworkMap(prefix string) *netmap.NetworkMap {
	self := (&tailcfg.Node{
		ID:       tailcfg.NodeID(1),
		StableID: tailcfg.StableNodeID(prefix + "-self"),
		Name:     prefix + ".tailnet.ts.net.",
		Hostinfo: (&tailcfg.Hostinfo{Hostname: prefix + "-host"}).View(),
		User:     tailcfg.UserID(7),
		Addresses: []netip.Prefix{
			netip.MustParsePrefix("100.64.0.1/32"),
			netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
		},
	}).View()
	peer := (&tailcfg.Node{
		ID:       tailcfg.NodeID(2),
		StableID: tailcfg.StableNodeID(prefix + "-peer"),
		Name:     "peer-" + prefix + ".tailnet.ts.net.",
		Hostinfo: (&tailcfg.Hostinfo{Hostname: "peer-" + prefix}).View(),
		User:     tailcfg.UserID(99),
		Addresses: []netip.Prefix{
			netip.MustParsePrefix("100.64.0.2/32"),
		},
	}).View()

	return &netmap.NetworkMap{
		SelfNode: self,
		Peers:    []tailcfg.NodeView{peer},
		UserProfiles: map[tailcfg.UserID]tailcfg.UserProfileView{
			tailcfg.UserID(7):  (&tailcfg.UserProfile{ID: 7, DisplayName: "Self Owner", LoginName: "self@example.com"}).View(),
			tailcfg.UserID(99): (&tailcfg.UserProfile{ID: 99, DisplayName: "Peer Owner", LoginName: "peer@example.com"}).View(),
		},
	}
}

func eventTypes(events []Event) []string {
	out := make([]string, 0, len(events))
	for _, event := range events {
		out = append(out, event.Type)
	}
	return out
}

func equalStrings(got, want []string) bool {
	if len(got) != len(want) {
		return false
	}
	for i := range got {
		if got[i] != want[i] {
			return false
		}
	}
	return true
}

func ptrTo[T any](v T) *T { return &v }
