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

package cli

import (
	"bytes"
	"net/netip"
	"testing"

	"github.com/google/go-cmp/cmp"
	"go4.org/mem"
	"tailscale.com/cmd/tailscale/cli/jsonoutput"
	"tailscale.com/ipn/ipnstate"
	"tailscale.com/tailcfg"
	"tailscale.com/tka"
	"tailscale.com/types/key"
	"tailscale.com/types/tkatype"
)

func TestTailnetLockLogOutput(t *testing.T) {
	votes := uint(1)
	aum1 := tka.AUM{
		MessageKind: tka.AUMAddKey,
		Key: &tka.Key{
			Kind:   tka.Key25519,
			Votes:  1,
			Public: []byte{2, 2},
		},
	}
	h1 := aum1.Hash()
	aum2 := tka.AUM{
		MessageKind: tka.AUMRemoveKey,
		KeyID:       []byte{3, 3},
		PrevAUMHash: h1[:],
		Signatures: []tkatype.Signature{
			{
				KeyID:     []byte{3, 4},
				Signature: []byte{4, 5},
			},
		},
		Meta: map[string]string{"en": "three", "de": "drei", "es": "tres"},
	}
	h2 := aum2.Hash()
	aum3 := tka.AUM{
		MessageKind: tka.AUMCheckpoint,
		PrevAUMHash: h2[:],
		State: &tka.State{
			Keys: []tka.Key{
				{
					Kind:   tka.Key25519,
					Votes:  1,
					Public: []byte{1, 1},
					Meta:   map[string]string{"en": "one", "de": "eins", "es": "uno"},
				},
			},
			DisablementValues: [][]byte{
				{1, 2, 3},
				{4, 5, 6},
				{7, 8, 9},
			},
		},
		Votes: &votes,
	}

	updates := []ipnstate.NetworkLockUpdate{
		{
			Hash:   aum3.Hash(),
			Change: aum3.MessageKind.String(),
			Raw:    aum3.Serialize(),
		},
		{
			Hash:   aum2.Hash(),
			Change: aum2.MessageKind.String(),
			Raw:    aum2.Serialize(),
		},
		{
			Hash:   aum1.Hash(),
			Change: aum1.MessageKind.String(),
			Raw:    aum1.Serialize(),
		},
	}

	t.Run("human-readable", func(t *testing.T) {
		t.Parallel()

		var outBuf bytes.Buffer
		json := jsonoutput.JSONSchemaVersion{}
		useColor := false

		printTailnetLockLog(updates, &outBuf, json, useColor)

		t.Logf("%s", outBuf.String())

		want := `update 4M4Q3IXBARPQMFVXHJBDCYQMWU5H5FBKD7MFF75HE4O5JMIWR2UA (checkpoint)
Disablement values:
 - 010203
 - 040506
 - 070809
Keys:
  Type: 25519
  KeyID: tlpub:0101
  Metadata: map[de:eins en:one es:uno]

update BKVVXHOVBW7Y7YXYTLVVLMNSYG6DS5GVRVSYZLASNU3AQKA732XQ (remove-key)
KeyID: tlpub:0303

update UKJIKFHILQ62AEN7MQIFHXJ6SFVDGQCQA3OHVI3LWVPM736EMSAA (add-key)
Type: 25519
KeyID: tlpub:0202

`

		if diff := cmp.Diff(outBuf.String(), want); diff != "" {
			t.Fatalf("wrong output (-got, +want):\n%s", diff)
		}
	})

	jsonV1 := `{
  "SchemaVersion": "1",
  "Messages": [
    {
      "Hash": "4M4Q3IXBARPQMFVXHJBDCYQMWU5H5FBKD7MFF75HE4O5JMIWR2UA",
      "AUM": {
        "MessageKind": "checkpoint",
        "PrevAUMHash": "BKVVXHOVBW7Y7YXYTLVVLMNSYG6DS5GVRVSYZLASNU3AQKA732XQ",
        "State": {
          "DisablementValues": [
            "010203",
            "040506",
            "070809"
          ],
          "Keys": [
            {
              "Kind": "25519",
              "Votes": 1,
              "Public": "tlpub:0101",
              "Meta": {
                "de": "eins",
                "en": "one",
                "es": "uno"
              }
            }
          ],
          "StateID1": 0,
          "StateID2": 0
        },
        "Votes": 1
      },
      "Raw": "pAEFAlggCqtbndUNv4_i-JrrVbGywbw5dNWNZYysEm02CCgf3q8FowH2AoNDAQIDQwQFBkMHCAkDgaQBAQIBA0IBAQyjYmRlZGVpbnNiZW5jb25lYmVzY3VubwYB"
    },
    {
      "Hash": "BKVVXHOVBW7Y7YXYTLVVLMNSYG6DS5GVRVSYZLASNU3AQKA732XQ",
      "AUM": {
        "MessageKind": "remove-key",
        "PrevAUMHash": "UKJIKFHILQ62AEN7MQIFHXJ6SFVDGQCQA3OHVI3LWVPM736EMSAA",
        "KeyID": "tlpub:0303",
        "Meta": {
          "de": "drei",
          "en": "three",
          "es": "tres"
        },
        "Signatures": [
          {
            "KeyID": "tlpub:0304",
            "Signature": "BAU="
          }
        ]
      },
      "Raw": "pQECAlggopKFFOhcPaARv2QQU90-kWozQFAG3Hqja7Vez-_EZIAEQgMDB6NiZGVkZHJlaWJlbmV0aHJlZWJlc2R0cmVzF4GiAUIDBAJCBAU="
    },
    {
      "Hash": "UKJIKFHILQ62AEN7MQIFHXJ6SFVDGQCQA3OHVI3LWVPM736EMSAA",
      "AUM": {
        "MessageKind": "add-key",
        "Key": {
          "Kind": "25519",
          "Votes": 1,
          "Public": "tlpub:0202"
        }
      },
      "Raw": "owEBAvYDowEBAgEDQgIC"
    }
  ]
}
`

	t.Run("json-1", func(t *testing.T) {
		t.Parallel()

		var outBuf bytes.Buffer
		json := jsonoutput.JSONSchemaVersion{
			IsSet: true,
			Value: 1,
		}
		useColor := false

		printTailnetLockLog(updates, &outBuf, json, useColor)

		want := jsonV1

		if diff := cmp.Diff(outBuf.String(), want); diff != "" {
			t.Fatalf("wrong output (-got, +want):\n%s", diff)
		}
	})
}

func TestTailnetLockStatusOutput(t *testing.T) {
	aum := tka.AUM{
		MessageKind: tka.AUMNoOp,
	}
	h := aum.Hash()
	head := [32]byte(h[:])

	nodeKey1 := key.NodePublicFromRaw32(mem.B(bytes.Repeat([]byte{1}, 32)))
	nodeKey2 := key.NodePublicFromRaw32(mem.B(bytes.Repeat([]byte{2}, 32)))
	nodeKey3 := key.NodePublicFromRaw32(mem.B(bytes.Repeat([]byte{3}, 32)))

	nlPub := key.NLPublicFromEd25519Unsafe(bytes.Repeat([]byte{4}, 32))

	trustedNlPub := key.NLPublicFromEd25519Unsafe(bytes.Repeat([]byte{5}, 32))

	tailnetIPv4_A, tailnetIPv6_A := netip.MustParseAddr("100.99.99.99"), netip.MustParseAddr("fd7a:115c:a1e0::701:b62a")
	tailnetIPv4_B, tailnetIPv6_B := netip.MustParseAddr("100.88.88.88"), netip.MustParseAddr("fd7a:115c:a1e0::4101:512f")

	t.Run("json-1", func(t *testing.T) {
		for _, tt := range []struct {
			Name   string
			Status ipnstate.NetworkLockStatus
			Want   string
		}{
			{
				Name:   "tailnet-lock-disabled",
				Status: ipnstate.NetworkLockStatus{Enabled: false},
				Want: `{
  "SchemaVersion": "1",
  "Enabled": false
}
`,
			},
			{
				Name: "tailnet-lock-disabled-with-keys",
				Status: ipnstate.NetworkLockStatus{
					Enabled:   false,
					NodeKey:   &nodeKey1,
					PublicKey: trustedNlPub,
				},
				Want: `{
  "SchemaVersion": "1",
  "Enabled": false,
  "PublicKey": "tlpub:0505050505050505050505050505050505050505050505050505050505050505",
  "NodeKey": "nodekey:0101010101010101010101010101010101010101010101010101010101010101"
}
`,
			},
			{
				Name: "tailnet-lock-enabled",
				Status: ipnstate.NetworkLockStatus{
					Enabled:          true,
					Head:             &head,
					PublicKey:        nlPub,
					NodeKey:          &nodeKey1,
					NodeKeySigned:    false,
					NodeKeySignature: nil,
					TrustedKeys: []ipnstate.TKAKey{
						{
							Kind:     tka.Key25519.String(),
							Votes:    1,
							Key:      trustedNlPub,
							Metadata: map[string]string{"en": "one", "de": "eins", "es": "uno"},
						},
					},
					VisiblePeers: []*ipnstate.TKAPeer{
						{
							Name:         "authentic-associate",
							ID:           tailcfg.NodeID(1234),
							StableID:     tailcfg.StableNodeID("1234_AAAA_TEST"),
							TailscaleIPs: []netip.Addr{tailnetIPv4_A, tailnetIPv6_A},
							NodeKey:      nodeKey2,
							NodeKeySignature: tka.NodeKeySignature{
								SigKind:        tka.SigDirect,
								Pubkey:         []byte("22222222222222222222222222222222"),
								KeyID:          []byte("44444444444444444444444444444444"),
								Signature:      []byte("1234567890"),
								WrappingPubkey: []byte("0987654321"),
							},
						},
					},
					FilteredPeers: []*ipnstate.TKAPeer{
						{
							Name:         "bogus-bandit",
							ID:           tailcfg.NodeID(5678),
							StableID:     tailcfg.StableNodeID("5678_BBBB_TEST"),
							TailscaleIPs: []netip.Addr{tailnetIPv4_B, tailnetIPv6_B},
							NodeKey:      nodeKey3,
						},
					},
					StateID: 98989898,
				},
				Want: `{
  "SchemaVersion": "1",
  "Enabled": true,
  "PublicKey": "tlpub:0404040404040404040404040404040404040404040404040404040404040404",
  "NodeKey": "nodekey:0101010101010101010101010101010101010101010101010101010101010101",
  "Head": "WYIVHDR7JUIXBWAJT5UPSCAILEXB7OMINDFEFEPOPNTUCNXMY2KA",
  "NodeKeySigned": false,
  "NodeKeySignature": null,
  "TrustedKeys": [
    {
      "Kind": "25519",
      "Votes": 1,
      "Public": "tlpub:0505050505050505050505050505050505050505050505050505050505050505",
      "Meta": {
        "de": "eins",
        "en": "one",
        "es": "uno"
      }
    }
  ],
  "VisiblePeers": [
    {
      "ID": "1234_AAAA_TEST",
      "DNSName": "authentic-associate",
      "TailscaleIPs": [
        "100.99.99.99",
        "fd7a:115c:a1e0::701:b62a"
      ],
      "NodeKey": "nodekey:0202020202020202020202020202020202020202020202020202020202020202",
      "NodeKeySignature": {
        "SigKind": "direct",
        "PublicKey": "tlpub:3232323232323232323232323232323232323232323232323232323232323232",
        "KeyID": "tlpub:3434343434343434343434343434343434343434343434343434343434343434",
        "Signature": "MTIzNDU2Nzg5MA==",
        "WrappingPublicKey": "tlpub:30393837363534333231"
      }
    }
  ],
  "FilteredPeers": [
    {
      "ID": "5678_BBBB_TEST",
      "DNSName": "bogus-bandit",
      "TailscaleIPs": [
        "100.88.88.88",
        "fd7a:115c:a1e0::4101:512f"
      ],
      "NodeKey": "nodekey:0303030303030303030303030303030303030303030303030303030303030303"
    }
  ],
  "State": 98989898
}
`,
			},
		} {
			t.Run(tt.Name, func(t *testing.T) {
				t.Parallel()

				var outBuf bytes.Buffer
				err := jsonoutput.PrintNetworkLockStatusJSONV1(&outBuf, &tt.Status)
				if err != nil {
					t.Fatalf("PrintNetworkLockStatusJSONV1: %v", err)
				}

				if diff := cmp.Diff(outBuf.String(), tt.Want); diff != "" {
					t.Fatalf("wrong output (-got, +want):\n%s", diff)
				}
			})
		}
	})
}
