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

package tsconsensus

import (
	"context"
	"fmt"
	"net/netip"
	"testing"

	"tailscale.com/ipn"
	"tailscale.com/ipn/ipnstate"
	"tailscale.com/tailcfg"
	"tailscale.com/types/key"
	"tailscale.com/types/views"
)

type testStatusGetter struct {
	status *ipnstate.Status
}

func (sg testStatusGetter) getStatus(ctx context.Context) (*ipnstate.Status, error) {
	return sg.status, nil
}

const testTag string = "tag:clusterTag"

func makeAuthTestPeer(i int, tags views.Slice[string]) *ipnstate.PeerStatus {
	return &ipnstate.PeerStatus{
		ID:   tailcfg.StableNodeID(fmt.Sprintf("%d", i)),
		Tags: &tags,
		TailscaleIPs: []netip.Addr{
			netip.AddrFrom4([4]byte{100, 0, 0, byte(i)}),
			netip.MustParseAddr(fmt.Sprintf("fd7a:115c:a1e0:0::%d", i)),
		},
	}
}

func makeAuthTestPeers(tags [][]string) []*ipnstate.PeerStatus {
	peers := make([]*ipnstate.PeerStatus, len(tags))
	for i, ts := range tags {
		peers[i] = makeAuthTestPeer(i, views.SliceOf(ts))
	}
	return peers
}

func authForStatus(s *ipnstate.Status) *authorization {
	return &authorization{
		sg: testStatusGetter{
			status: s,
		},
		tag: testTag,
	}
}

func authForPeers(self *ipnstate.PeerStatus, peers []*ipnstate.PeerStatus) *authorization {
	s := &ipnstate.Status{
		BackendState: ipn.Running.String(),
		Self:         self,
		Peer:         map[key.NodePublic]*ipnstate.PeerStatus{},
	}
	for _, p := range peers {
		s.Peer[key.NewNode().Public()] = p
	}
	return authForStatus(s)
}

func TestAuthRefreshErrorsNotRunning(t *testing.T) {
	tests := []struct {
		name     string
		in       *ipnstate.Status
		expected string
	}{
		{
			name:     "no-status",
			in:       nil,
			expected: "no status",
		},
		{
			name: "ts-server-not-running",
			in: &ipnstate.Status{
				BackendState: "NeedsMachineAuth",
			},
			expected: "ts Server is not running",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := t.Context()
			a := authForStatus(tt.in)
			err := a.Refresh(ctx)
			if err == nil {
				t.Fatalf("expected err to be non-nil")
			}
			if err.Error() != tt.expected {
				t.Fatalf("expected: %s, got: %s", tt.expected, err.Error())
			}
		})
	}
}

func TestAuthUnrefreshed(t *testing.T) {
	a := authForStatus(nil)
	if a.AllowsHost(netip.MustParseAddr("100.0.0.1")) {
		t.Fatalf("never refreshed authorization, allowsHost: expected false, got true")
	}
	gotAllowedPeers := a.AllowedPeers()
	if gotAllowedPeers.Len() != 0 {
		t.Fatalf("never refreshed authorization, allowedPeers: expected [], got %v", gotAllowedPeers)
	}
	if a.SelfAllowed() != false {
		t.Fatalf("never refreshed authorization, selfAllowed: expected false got true")
	}
}

func TestAuthAllowsHost(t *testing.T) {
	peerTags := [][]string{
		{"woo"},
		nil,
		{"woo", testTag},
		{testTag},
	}
	peers := makeAuthTestPeers(peerTags)

	tests := []struct {
		name       string
		peerStatus *ipnstate.PeerStatus
		expected   bool
	}{
		{
			name:       "tagged-different-tag",
			peerStatus: peers[0],
			expected:   false,
		},
		{
			name:       "not-tagged",
			peerStatus: peers[1],
			expected:   false,
		},
		{
			name:       "tags-include-testTag",
			peerStatus: peers[2],
			expected:   true,
		},
		{
			name:       "only-testTag",
			peerStatus: peers[3],
			expected:   true,
		},
	}

	a := authForPeers(nil, peers)
	err := a.Refresh(t.Context())
	if err != nil {
		t.Fatal(err)
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// test we get the expected result for any of the peers TailscaleIPs
			for _, addr := range tt.peerStatus.TailscaleIPs {
				got := a.AllowsHost(addr)
				if got != tt.expected {
					t.Fatalf("allowed for peer with tags: %v, expected: %t, got %t", tt.peerStatus.Tags, tt.expected, got)
				}
			}
		})
	}
}

func TestAuthAllowedPeers(t *testing.T) {
	ctx := t.Context()
	peerTags := [][]string{
		{"woo"},
		nil,
		{"woo", testTag},
		{testTag},
	}
	peers := makeAuthTestPeers(peerTags)
	a := authForPeers(nil, peers)
	err := a.Refresh(ctx)
	if err != nil {
		t.Fatal(err)
	}
	ps := a.AllowedPeers()
	if ps.Len() != 2 {
		t.Fatalf("expected: 2, got: %d", ps.Len())
	}
	for _, i := range []int{2, 3} {
		if !ps.ContainsFunc(func(p *ipnstate.PeerStatus) bool {
			return p.ID == peers[i].ID
		}) {
			t.Fatalf("expected peers[%d] to be in AllowedPeers because it is tagged with testTag", i)
		}
	}
}

func TestAuthSelfAllowed(t *testing.T) {
	tests := []struct {
		name     string
		in       []string
		expected bool
	}{
		{
			name:     "self-different-tag",
			in:       []string{"woo"},
			expected: false,
		},
		{
			name:     "self-tags-include-testTag",
			in:       []string{"woo", testTag},
			expected: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := t.Context()
			self := makeAuthTestPeer(0, views.SliceOf(tt.in))
			a := authForPeers(self, nil)
			err := a.Refresh(ctx)
			if err != nil {
				t.Fatal(err)
			}
			got := a.SelfAllowed()
			if got != tt.expected {
				t.Fatalf("expected: %t, got: %t", tt.expected, got)
			}
		})
	}
}
