// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

package ipnlocal

import (
	"net/netip"
	"testing"

	"tailscale.com/tailcfg"
	"tailscale.com/tstest"
	"tailscale.com/types/netmap"
	"tailscale.com/util/eventbus"
	"tailscale.com/wgengine/magicsock"
)

// newCacheTestNetmap returns a minimal valid netmap suitable for testing disk
// cache operations.
func newCacheTestNetmap() *netmap.NetworkMap {
	return &netmap.NetworkMap{
		SelfNode: (&tailcfg.Node{
			Name: "test-node.ts.net",
			User: tailcfg.UserID(1),
			Addresses: []netip.Prefix{
				netip.MustParsePrefix("100.64.0.1/32"),
			},
		}).View(),
		UserProfiles: map[tailcfg.UserID]tailcfg.UserProfileView{
			tailcfg.UserID(1): (&tailcfg.UserProfile{
				LoginName:   "user@example.com",
				DisplayName: "Test User",
			}).View(),
		},
		DERPMap: &tailcfg.DERPMap{
			Regions: map[int]*tailcfg.DERPRegion{
				1:  {},
				2:  {},
				3:  {},
				4:  {},
				5:  {},
				6:  {},
				7:  {},
				8:  {},
				9:  {},
				10: {},
				11: {},
			},
		},
	}
}

func TestWriteAndLoadHomeDERP(t *testing.T) {
	b := newTestBackend(t)

	nm := newCacheTestNetmap()
	b.currentNode().SetNetMap(nm)

	const wantDERP = 7
	b.currentNode().homeDERP.Store(wantDERP)

	b.mu.Lock()
	defer b.mu.Unlock()

	if err := b.writeNetmapToDiskLockedWithPeers(nm); err != nil {
		t.Fatalf("writeNetmapToDiskLocked: %v", err)
	}

	loaded, ok := b.loadDiskCacheLocked()
	if !ok {
		t.Fatal("loadDiskCacheLocked returned ok=false")
	}
	if !loaded.SelfNode.Valid() {
		t.Fatal("loaded netmap SelfNode is invalid")
	}
	if got := loaded.SelfNode.HomeDERP(); got != wantDERP {
		t.Errorf("loaded SelfNode.HomeDERP() = %d, want %d", got, wantDERP)
	}
}

func TestOnHomeDERPUpdate(t *testing.T) {
	t.Run("normal_derp_change", func(t *testing.T) {
		b := newTestBackend(t)
		done := make(chan struct{})
		tstest.Replace(t, &testOnlyHomeDERPUpdate, func() { close(done) })

		nm := newCacheTestNetmap()
		b.currentNode().SetNetMap(nm)

		// Publish a HomeDERPChanged event via the backend's event bus.
		bus := b.Sys().Bus.Get()
		ec := bus.Client("test.TestOnHomeDERPUpdate")
		pub := eventbus.Publish[magicsock.HomeDERPChanged](ec)

		const wantDERP = 11
		pub.Publish(magicsock.HomeDERPChanged{Old: 0, New: wantDERP})
		<-done

		if got := b.currentNode().homeDERP.Load(); got != wantDERP {
			t.Errorf("b.homeDERP = %d, want %d", got, wantDERP)
		}

		// Verify the value was persisted to the disk cache.
		b.mu.Lock()
		defer b.mu.Unlock()
		loaded, ok := b.loadDiskCacheLocked()
		if !ok {
			t.Fatal("loadDiskCacheLocked returned ok=false after homeDERP update")
		}
		if got := loaded.SelfNode.HomeDERP(); got != wantDERP {
			t.Errorf("cached SelfNode.HomeDERP() = %d, want %d", got, wantDERP)
		}
	})
	t.Run("old_does_not_match", func(t *testing.T) {
		b := newTestBackend(t)
		done := make(chan struct{})
		tstest.Replace(t, &testOnlyHomeDERPUpdate, func() { close(done) })

		const setDERP = 11
		const wantDERP = 4

		nm := newCacheTestNetmap()
		selfNode := nm.SelfNode.AsStruct()
		selfNode.HomeDERP = wantDERP
		nm.SelfNode = selfNode.View()
		b.currentNode().SetNetMap(nm)
		b.currentNode().homeDERP.Store(wantDERP)

		// Write an initial cache entry so we can verify it is not overwritten.
		b.mu.Lock()
		if err := b.writeNetmapToDiskLockedWithPeers(nm); err != nil {
			b.mu.Unlock()
			t.Fatalf("setup writeNetmapToDiskLocked: %v", err)
		}
		b.mu.Unlock()

		// Publish a HomeDERPChanged event via the backend's event bus.
		bus := b.Sys().Bus.Get()
		ec := bus.Client("test.TestOnHomeDERPUpdate")
		pub := eventbus.Publish[magicsock.HomeDERPChanged](ec)
		pub.Publish(magicsock.HomeDERPChanged{Old: wantDERP + 1, New: setDERP})
		<-done

		if got := b.currentNode().homeDERP.Load(); got != wantDERP {
			t.Errorf("b.homeDERP = %d, wanted no change %d", got, wantDERP)
		}

		// Verify the cache still exists and still holds the original value.
		b.mu.Lock()
		defer b.mu.Unlock()
		loaded, ok := b.loadDiskCacheLocked()
		if !ok {
			t.Fatal("loadDiskCacheLocked returned ok=false; expected cache to still exist")
		}
		if got := loaded.SelfNode.HomeDERP(); got != wantDERP {
			t.Errorf("cached SelfNode.HomeDERP() = %d after rejected event, want original %d", got, wantDERP)
		}
	})
	t.Run("new_does_not_exist_in_map", func(t *testing.T) {
		b := newTestBackend(t)
		done := make(chan struct{})
		tstest.Replace(t, &testOnlyHomeDERPUpdate, func() { close(done) })

		const setDERP = 111
		const wantDERP = 4

		nm := newCacheTestNetmap()
		selfNode := nm.SelfNode.AsStruct()
		selfNode.HomeDERP = wantDERP
		nm.SelfNode = selfNode.View()
		b.currentNode().SetNetMap(nm)
		b.currentNode().homeDERP.Store(wantDERP)

		// Write an initial cache entry so we can verify it is not overwritten.
		b.mu.Lock()
		if err := b.writeNetmapToDiskLockedWithPeers(nm); err != nil {
			b.mu.Unlock()
			t.Fatalf("setup writeNetmapToDiskLocked: %v", err)
		}
		b.mu.Unlock()

		// Publish a HomeDERPChanged event via the backend's event bus.
		// Old matches the stored homeDERP so only the "new region not in map"
		// guard is exercised.
		bus := b.Sys().Bus.Get()
		ec := bus.Client("test.TestOnHomeDERPUpdate")
		pub := eventbus.Publish[magicsock.HomeDERPChanged](ec)
		pub.Publish(magicsock.HomeDERPChanged{Old: wantDERP, New: setDERP})
		<-done

		if got := b.currentNode().homeDERP.Load(); got != wantDERP {
			t.Errorf("b.homeDERP = %d, wanted no change %d", got, wantDERP)
		}

		// Verify the cache still exists and still holds the original value.
		b.mu.Lock()
		defer b.mu.Unlock()
		loaded, ok := b.loadDiskCacheLocked()
		if !ok {
			t.Fatal("loadDiskCacheLocked returned ok=false; expected cache to still exist")
		}
		if got := loaded.SelfNode.HomeDERP(); got != wantDERP {
			t.Errorf("cached SelfNode.HomeDERP() = %d after rejected event, want original %d", got, wantDERP)
		}
	})
}

func TestWriteNetmapDoesNotMutateOriginal(t *testing.T) {
	b := newTestBackend(t)

	nm := newCacheTestNetmap()
	b.currentNode().SetNetMap(nm)

	originalDERP := nm.SelfNode.HomeDERP() // expected to be 0 initially

	const storeDERP = 5
	b.currentNode().homeDERP.Store(storeDERP)

	b.mu.Lock()
	defer b.mu.Unlock()

	if err := b.writeNetmapToDiskLockedWithPeers(nm); err != nil {
		t.Fatalf("writeNetmapToDiskLocked: %v", err)
	}

	// The original netmap must not have been mutated.
	if got := nm.SelfNode.HomeDERP(); got != originalDERP {
		t.Errorf("original nm.SelfNode.HomeDERP() = %d after write, want %d (original was mutated)", got, originalDERP)
	}
}
