// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package server

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"net/url"
	"os"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/nats-io/jwt/v2"
	"github.com/nats-io/nats.go"
	"github.com/nats-io/nkeys"
)

func TestUserCloneNilPermissions(t *testing.T) {
	user := &User{
		Username: "foo",
		Password: "bar",
	}

	clone := user.clone()

	if !reflect.DeepEqual(user, clone) {
		t.Fatalf("Cloned Users are incorrect.\nexpected: %+v\ngot: %+v",
			user, clone)
	}

	clone.Password = "baz"
	if reflect.DeepEqual(user, clone) {
		t.Fatal("Expected Users to be different")
	}
}

func TestUserClone(t *testing.T) {
	user := &User{
		Username: "foo",
		Password: "bar",
		Permissions: &Permissions{
			Publish: &SubjectPermission{
				Allow: []string{"foo"},
			},
			Subscribe: &SubjectPermission{
				Allow: []string{"bar"},
			},
		},
	}

	clone := user.clone()

	if !reflect.DeepEqual(user, clone) {
		t.Fatalf("Cloned Users are incorrect.\nexpected: %+v\ngot: %+v",
			user, clone)
	}

	clone.Permissions.Subscribe.Allow = []string{"baz"}
	if reflect.DeepEqual(user, clone) {
		t.Fatal("Expected Users to be different")
	}
}

func TestUserClonePermissionsNoLists(t *testing.T) {
	user := &User{
		Username:    "foo",
		Password:    "bar",
		Permissions: &Permissions{},
	}

	clone := user.clone()

	if clone.Permissions.Publish != nil {
		t.Fatalf("Expected Publish to be nil, got: %v", clone.Permissions.Publish)
	}
	if clone.Permissions.Subscribe != nil {
		t.Fatalf("Expected Subscribe to be nil, got: %v", clone.Permissions.Subscribe)
	}
}

func TestUserCloneNoPermissions(t *testing.T) {
	user := &User{
		Username: "foo",
		Password: "bar",
	}

	clone := user.clone()

	if clone.Permissions != nil {
		t.Fatalf("Expected Permissions to be nil, got: %v", clone.Permissions)
	}
}

func TestUserCloneNil(t *testing.T) {
	user := (*User)(nil)
	clone := user.clone()
	if clone != nil {
		t.Fatalf("Expected nil, got: %+v", clone)
	}
}

func TestUserUnknownAllowedConnectionType(t *testing.T) {
	o := DefaultOptions()
	o.Users = []*User{{
		Username:               "user",
		Password:               "pwd",
		AllowedConnectionTypes: testCreateAllowedConnectionTypes([]string{jwt.ConnectionTypeStandard, "someNewType"}),
	}}
	_, err := NewServer(o)
	if err == nil || !strings.Contains(err.Error(), "connection type") {
		t.Fatalf("Expected error about unknown connection type, got %v", err)
	}

	o.Users[0].AllowedConnectionTypes = testCreateAllowedConnectionTypes([]string{"websocket"})
	s, err := NewServer(o)
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}
	s.mu.Lock()
	user := s.opts.Users[0]
	s.mu.Unlock()
	for act := range user.AllowedConnectionTypes {
		if act != jwt.ConnectionTypeWebsocket {
			t.Fatalf("Expected map to have been updated with proper case, got %v", act)
		}
	}
	// Same with NKey user now.
	o.Users = nil
	o.Nkeys = []*NkeyUser{{
		Nkey:                   "somekey",
		AllowedConnectionTypes: testCreateAllowedConnectionTypes([]string{jwt.ConnectionTypeStandard, "someNewType"}),
	}}
	_, err = NewServer(o)
	if err == nil || !strings.Contains(err.Error(), "connection type") {
		t.Fatalf("Expected error about unknown connection type, got %v", err)
	}
	o.Nkeys[0].AllowedConnectionTypes = testCreateAllowedConnectionTypes([]string{"websocket"})
	s, err = NewServer(o)
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}
	s.mu.Lock()
	nkey := s.opts.Nkeys[0]
	s.mu.Unlock()
	for act := range nkey.AllowedConnectionTypes {
		if act != jwt.ConnectionTypeWebsocket {
			t.Fatalf("Expected map to have been updated with proper case, got %v", act)
		}
	}
}

func TestDNSAltNameMatching(t *testing.T) {
	for idx, test := range []struct {
		altName string
		urls    []string
		match   bool
	}{
		{"foo", []string{"FOO"}, true},
		{"foo", []string{".."}, false},
		{"foo", []string{"."}, false},
		{"Foo", []string{"foO"}, true},
		{"FOO", []string{"foo"}, true},
		{"foo1", []string{"bar"}, false},
		{"multi", []string{"m", "mu", "mul", "multi"}, true},
		{"multi", []string{"multi", "m", "mu", "mul"}, true},
		{"foo.bar", []string{"foo", "foo.bar.bar", "foo.baz"}, false},
		{"foo.Bar", []string{"foo", "bar.foo", "Foo.Bar"}, true},
		{"foo.*", []string{"foo", "bar.foo", "Foo.Bar"}, false}, // only match left most
		{"f*.bar", []string{"foo", "bar.foo", "Foo.Bar"}, false},
		{"*.bar", []string{"foo.bar"}, true},
		{"*", []string{"baz.bar", "bar", "z.y"}, true},
		{"*", []string{"bar"}, true},
		{"*", []string{"."}, false},
		{"*", []string{""}, true},
		{"*", []string{"*"}, true},
		{"bar.*", []string{"bar.*"}, true},
		{"*.Y-X-red-mgmt.default.svc", []string{"A.Y-X-red-mgmt.default.svc"}, true},
		{"*.Y-X-green-mgmt.default.svc", []string{"A.Y-X-green-mgmt.default.svc"}, true},
		{"*.Y-X-blue-mgmt.default.svc", []string{"A.Y-X-blue-mgmt.default.svc"}, true},
		{"Y-X-red-mgmt", []string{"Y-X-red-mgmt"}, true},
		{"Y-X-red-mgmt", []string{"X-X-red-mgmt"}, false},
		{"Y-X-red-mgmt", []string{"Y-X-green-mgmt"}, false},
		{"Y-X-red-mgmt", []string{"Y"}, false},
		{"Y-X-red-mgmt", []string{"Y-X"}, false},
		{"Y-X-red-mgmt", []string{"Y-X-red"}, false},
		{"Y-X-red-mgmt", []string{"X-red-mgmt"}, false},
		{"Y-X-green-mgmt", []string{"Y-X-green-mgmt"}, true},
		{"Y-X-blue-mgmt", []string{"Y-X-blue-mgmt"}, true},
		{"connect.Y.local", []string{"connect.Y.local"}, true},
		{"connect.Y.local", []string{".Y.local"}, false},
		{"connect.Y.local", []string{"..local"}, false},
		{"gcp.Y.local", []string{"gcp.Y.local"}, true},
		{"uswest1.gcp.Y.local", []string{"uswest1.gcp.Y.local"}, true},
	} {
		urlSet := make([]*url.URL, len(test.urls))
		for i, u := range test.urls {
			var err error
			urlSet[i], err = url.Parse("nats://" + u)
			if err != nil {
				t.Fatal(err)
			}
		}
		if dnsAltNameMatches(dnsAltNameLabels(test.altName), urlSet) != test.match {
			t.Fatal("Test", idx, "Match miss match, expected:", test.match)
		}
	}
}

func TestNoAuthUser(t *testing.T) {
	conf := createConfFile(t, []byte(`
		listen: "127.0.0.1:-1"
		accounts {
			FOO { users [{user: "foo", password: "pwd1"}] }
			BAR { users [{user: "bar", password: "pwd2"}] }
		}
		no_auth_user: "foo"
	`))
	defer os.Remove(conf)
	s, o := RunServerWithConfig(conf)
	defer s.Shutdown()

	for _, test := range []struct {
		name    string
		usrInfo string
		ok      bool
		account string
	}{
		{"valid user/pwd", "bar:pwd2@", true, "BAR"},
		{"invalid pwd", "bar:wrong@", false, _EMPTY_},
		{"some token", "sometoken@", false, _EMPTY_},
		{"user used without pwd", "bar@", false, _EMPTY_}, // will be treated as a token
		{"user with empty password", "bar:@", false, _EMPTY_},
		{"no user", _EMPTY_, true, "FOO"},
	} {
		t.Run(test.name, func(t *testing.T) {
			url := fmt.Sprintf("nats://%s127.0.0.1:%d", test.usrInfo, o.Port)
			nc, err := nats.Connect(url)
			if err != nil {
				if test.ok {
					t.Fatalf("Unexpected error: %v", err)
				}
				return
			} else if !test.ok {
				nc.Close()
				t.Fatalf("Should have failed, did not")
			}
			var accName string
			s.mu.Lock()
			for _, c := range s.clients {
				c.mu.Lock()
				if c.acc != nil {
					accName = c.acc.Name
				}
				c.mu.Unlock()
				break
			}
			s.mu.Unlock()
			nc.Close()
			checkClientsCount(t, s, 0)
			if accName != test.account {
				t.Fatalf("The account should have been %q, got %q", test.account, accName)
			}
		})
	}
}

func TestNoAuthUserNkey(t *testing.T) {
	conf := createConfFile(t, []byte(`
		listen: "127.0.0.1:-1"
		accounts {
			FOO { users [{user: "foo", password: "pwd1"}] }
			BAR { users [{nkey: "UBO2MQV67TQTVIRV3XFTEZOACM4WLOCMCDMAWN5QVN5PI2N6JHTVDRON"}] }
		}
		no_auth_user: "UBO2MQV67TQTVIRV3XFTEZOACM4WLOCMCDMAWN5QVN5PI2N6JHTVDRON"
	`))
	s, _ := RunServerWithConfig(conf)
	defer s.Shutdown()

	// Make sure we connect ok and to the correct account.
	nc := natsConnect(t, s.ClientURL())
	defer nc.Close()
	resp, err := nc.Request(userDirectInfoSubj, nil, time.Second)
	require_NoError(t, err)
	response := ServerAPIResponse{Data: &UserInfo{}}
	err = json.Unmarshal(resp.Data, &response)
	require_NoError(t, err)
	userInfo := response.Data.(*UserInfo)
	require_Equal(t, userInfo.UserID, "UBO2MQV67TQTVIRV3XFTEZOACM4WLOCMCDMAWN5QVN5PI2N6JHTVDRON")
	require_Equal(t, userInfo.Account, "BAR")
}

func TestUserConnectionDeadline(t *testing.T) {
	clientAuth := &DummyAuth{
		t:        t,
		register: true,
		deadline: time.Now().Add(50 * time.Millisecond),
	}

	opts := DefaultOptions()
	opts.CustomClientAuthentication = clientAuth

	s := RunServer(opts)
	defer s.Shutdown()

	var dcerr error

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)

	nc, err := nats.Connect(
		s.ClientURL(),
		nats.UserInfo("valid", _EMPTY_),
		nats.NoReconnect(),
		nats.ErrorHandler(func(nc *nats.Conn, _ *nats.Subscription, err error) {
			dcerr = err
			cancel()
		}))
	if err != nil {
		t.Fatalf("Expected client to connect, got: %s", err)
	}

	<-ctx.Done()

	checkFor(t, 2*time.Second, 100*time.Millisecond, func() error {
		if nc.IsConnected() {
			return fmt.Errorf("Expected to be disconnected")
		}
		return nil
	})

	if dcerr == nil || dcerr.Error() != "nats: authentication expired" {
		t.Fatalf("Expected a auth expired error: got: %v", dcerr)
	}
}

func TestNoAuthUserNoConnectProto(t *testing.T) {
	conf := createConfFile(t, []byte(`
		listen: "127.0.0.1:-1"
		accounts {
			A { users [{user: "foo", password: "pwd"}] }
		}
		authorization { timeout: 1 }
		no_auth_user: "foo"
	`))
	defer os.Remove(conf)
	s, o := RunServerWithConfig(conf)
	defer s.Shutdown()

	checkClients := func(n int) {
		t.Helper()
		time.Sleep(100 * time.Millisecond)
		if nc := s.NumClients(); nc != n {
			t.Fatalf("Expected %d clients, got %d", n, nc)
		}
	}

	conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", o.Host, o.Port))
	require_NoError(t, err)
	defer conn.Close()
	checkClientsCount(t, s, 1)

	// With no auth user we should not require a CONNECT.
	// Make sure we are good on not sending CONN first.
	_, err = conn.Write([]byte("PUB foo 2\r\nok\r\n"))
	require_NoError(t, err)
	checkClients(1)
	conn.Close()

	// Now make sure we still do get timed out though.
	conn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", o.Host, o.Port))
	require_NoError(t, err)
	defer conn.Close()
	checkClientsCount(t, s, 1)

	time.Sleep(1200 * time.Millisecond)
	checkClientsCount(t, s, 0)
}

type captureProxyRequiredLogger struct {
	DummyLogger
	ch chan string
}

func (l *captureProxyRequiredLogger) Debugf(format string, args ...any) {
	msg := fmt.Sprintf(format, args...)
	if strings.Contains(msg, ErrAuthProxyRequired.Error()) {
		select {
		case l.ch <- msg:
		default:
		}
	}
}

func TestAuthProxyRequired(t *testing.T) {
	conf := createConfFile(t, []byte(`
		port: -1
		authorization {
			user: user
			password: pwd
			proxy_required: true
		}
	`))
	s, _ := RunServerWithConfig(conf)
	defer s.Shutdown()

	l := &captureProxyRequiredLogger{ch: make(chan string, 1)}
	s.SetLogger(l, true, false)

	_, err := nats.Connect(s.ClientURL(), nats.UserInfo("user", "pwd"))
	require_True(t, errors.Is(err, nats.ErrAuthorization))

	checkLog := func() {
		t.Helper()
		select {
		case <-l.ch:
			return
		case <-time.After(time.Second):
			t.Fatal("Did not get log statement")
		}
	}
	checkLog()

	drainLog := func() {
		for {
			select {
			case <-l.ch:
			default:
				return
			}
		}
	}
	s.Shutdown()
	drainLog()

	nkUsr1, err := nkeys.CreateUser()
	require_NoError(t, err)
	nkPub1, err := nkUsr1.PublicKey()
	require_NoError(t, err)

	nkUsr2, err := nkeys.CreateUser()
	require_NoError(t, err)
	nkPub2, err := nkUsr2.PublicKey()
	require_NoError(t, err)

	conf = createConfFile(t, fmt.Appendf(nil, `
		port: -1
		authorization {
			users: [
				{user: user1, password: pwd1}
				{user: user2, password: pwd2, proxy_required: true}
				{user: user3, password: pwd3, proxy_required: false}
				{nkey: "%s", proxy_required: true}
				{nkey: "%s", proxy_required: false}
			]
		}
	`, nkPub1, nkPub2))
	s, _ = RunServerWithConfig(conf)
	defer s.Shutdown()

	s.SetLogger(l, true, false)

	checkClients := func() {
		t.Helper()
		// Should connect ok.
		nc := natsConnect(t, s.ClientURL(), nats.UserInfo("user1", "pwd1"))
		nc.Close()

		// Should not, since it requires going through proxy.
		_, err = nats.Connect(s.ClientURL(), nats.UserInfo("user2", "pwd2"))
		require_True(t, errors.Is(err, nats.ErrAuthorization))
		checkLog()

		// Should connect ok.
		nc = natsConnect(t, s.ClientURL(), nats.UserInfo("user3", "pwd3"))
		nc.Close()

		// Should not, since it requires going through proxy.
		_, err = nats.Connect(s.ClientURL(), nats.Nkey(nkPub1, func(nonce []byte) ([]byte, error) {
			return nkUsr1.Sign(nonce)
		}))
		require_True(t, errors.Is(err, nats.ErrAuthorization))
		checkLog()

		// Should connect ok.
		nc = natsConnect(t, s.ClientURL(), nats.Nkey(nkPub2, func(nonce []byte) ([]byte, error) {
			return nkUsr2.Sign(nonce)
		}))
		nc.Close()
	}
	checkClients()
	s.Shutdown()
	drainLog()

	conf = createConfFile(t, fmt.Appendf(nil, `
		port: -1
		accounts {
			A {
				users: [
					{user: user1, password: pwd1}
					{user: user2, password: pwd2, proxy_required: true}
					{user: user3, password: pwd3, proxy_required: false}
					{nkey: "%s", proxy_required: true}
					{nkey: "%s", proxy_required: false}
				]
			}
			SYS { users: [ {user:sys, password: pwd} ] }
		}
		system_account: SYS
	`, nkPub1, nkPub2))
	s, _ = RunServerWithConfig(conf)
	defer s.Shutdown()

	s.SetLogger(l, true, false)

	snc := natsConnect(t, s.ClientURL(), nats.UserInfo("sys", "pwd"))
	defer snc.Close()
	sub := natsSubSync(t, snc, "$SYS.SERVER.*.CLIENT.AUTH.ERR")
	sub2 := natsSubSync(t, snc, "$SYS.ACCOUNT.A.DISCONNECT")
	natsFlush(t, snc)

	checkClients()

	// We should get 2 authentication error messages, saying that
	// the reason is "ProxyRequired".
	for range 2 {
		var de DisconnectEventMsg
		msg := natsNexMsg(t, sub, time.Second)
		err = json.Unmarshal(msg.Data, &de)
		require_NoError(t, err)
		require_Equal(t, de.Reason, ProxyRequired.String())
	}

	// We should get 3 disconnect events since only 3 have connected
	// ok and then just closed.
	for range 3 {
		var de DisconnectEventMsg
		msg := natsNexMsg(t, sub2, time.Second)
		err = json.Unmarshal(msg.Data, &de)
		require_NoError(t, err)
		require_Equal(t, de.Reason, ClientClosed.String())
	}
	// Make sure there is no more.
	if msg, err := sub2.NextMsg(100 * time.Millisecond); err != nats.ErrTimeout {
		t.Fatalf("Should have received only 3 disconnect messages, got another: %s", msg.Data)
	}

	s.Shutdown()
	drainLog()

	conf = createConfFile(t, []byte(`
		port: -1
		leafnodes: {
			port: -1
			authorization {
				user: user
				password: pwd
				proxy_required: true
			}
		}
	`))
	s, o := RunServerWithConfig(conf)
	defer s.Shutdown()

	s.SetLogger(l, true, false)

	lconf := createConfFile(t, fmt.Appendf(nil, `
		port: -1
		leafnodes {
			reconnect_interval: "50ms"
			remotes [ {url: "nats://user:pwd@127.0.0.1:%d"} ]
		}
	`, o.LeafNode.Port))
	leaf, _ := RunServerWithConfig(lconf)
	defer leaf.Shutdown()

	time.Sleep(125 * time.Millisecond)
	checkLeafNodeConnectedCount(t, leaf, 0)
	checkLog()
	leaf.Shutdown()
	s.Shutdown()
	drainLog()

	conf = createConfFile(t, []byte(`
		port: -1
		leafnodes: {
			port: -1
			reconnect_interval: "50ms"
			authorization {
				users: [
					{user: user1, password: pwd1}
					{user: user2, password: pwd2, proxy_required: true}
					{user: user3, password: pwd3, proxy_required: false}
				]
			}
		}
	`))
	s, o = RunServerWithConfig(conf)
	defer s.Shutdown()

	s.SetLogger(l, true, false)

	lconf = createConfFile(t, fmt.Appendf(nil, `
		port: -1
		leafnodes {
			reconnect_interval: "50ms"
			remotes [ {url: "nats://user2:pwd@127.0.0.1:%d"} ]
		}
	`, o.LeafNode.Port))
	leaf, _ = RunServerWithConfig(lconf)
	defer leaf.Shutdown()

	time.Sleep(125 * time.Millisecond)
	checkLeafNodeConnectedCount(t, leaf, 0)
	checkLog()
	leaf.Shutdown()
	s.Shutdown()
	drainLog()

	conf = createConfFile(t, fmt.Appendf(nil, `
		port: -1
		leafnodes: {
			port: -1
			authorization {
				nkey: %s
				proxy_required: true
			}
		}
	`, nkPub1))
	s, o = RunServerWithConfig(conf)
	defer s.Shutdown()

	s.SetLogger(l, true, false)

	nkSeed1, err := nkUsr1.Seed()
	require_NoError(t, err)

	lconf = createConfFile(t, fmt.Appendf(nil, `
		port: -1
		leafnodes {
			reconnect_interval: "50ms"
			remotes [
				{
					url: "nats://127.0.0.1:%d"
					nkey: "%s"
				}
			]
		}
	`, o.LeafNode.Port, nkSeed1))
	leaf, _ = RunServerWithConfig(lconf)
	defer leaf.Shutdown()

	time.Sleep(125 * time.Millisecond)
	checkLeafNodeConnectedCount(t, leaf, 0)
	checkLog()
	leaf.Shutdown()
	s.Shutdown()
	drainLog()

	// Check with operator mode.
	conf = createConfFile(t, []byte(`
		port: -1
		server_name: OP
		operator = "../test/configs/nkeys/op.jwt"
		resolver = MEMORY
		listen: "127.0.0.1:-1"
		leafnodes {
			listen: "127.0.0.1:-1"
		}
	`))
	s, o = RunServerWithConfig(conf)
	defer s.Shutdown()

	s.SetLogger(l, true, false)

	_, akp := createAccount(s)
	kp, err := nkeys.CreateUser()
	require_NoError(t, err)
	pub, err := kp.PublicKey()
	require_NoError(t, err)
	nuc := jwt.NewUserClaims(pub)
	nuc.ProxyRequired = true
	ujwt, err := nuc.Encode(akp)
	require_NoError(t, err)

	lopts := &DefaultTestOptions
	u, err := url.Parse(fmt.Sprintf("nats://%s:%d", o.LeafNode.Host, o.LeafNode.Port))
	require_NoError(t, err)
	remote := &RemoteLeafOpts{URLs: []*url.URL{u}}
	remote.SignatureCB = func(nonce []byte) (string, []byte, error) {
		sig, err := kp.Sign(nonce)
		return ujwt, sig, err
	}
	lopts.LeafNode.Remotes = []*RemoteLeafOpts{remote}
	lopts.LeafNode.ReconnectInterval = 100 * time.Millisecond
	leaf = RunServer(lopts)
	defer leaf.Shutdown()

	time.Sleep(125 * time.Millisecond)
	checkLeafNodeConnectedCount(t, leaf, 0)
	checkLog()
	leaf.Shutdown()

	// Try with an user.
	drainLog()

	_, err = nats.Connect(s.ClientURL(), createUserCredsEx(t, nuc, akp))
	require_True(t, errors.Is(err, nats.ErrAuthorization))
	checkLog()

	drainLog()

	// Try with creds file.
	kp, err = nkeys.CreateUser()
	require_NoError(t, err)
	useed, err := kp.Seed()
	require_NoError(t, err)
	pub, err = kp.PublicKey()
	require_NoError(t, err)
	nuc = jwt.NewUserClaims(pub)
	nuc.ProxyRequired = true
	ujwt, err = nuc.Encode(akp)
	require_NoError(t, err)
	credsFile := genCredsFile(t, ujwt, useed)

	lconf = createConfFile(t, fmt.Appendf(nil, `
		port: -1
		leafnodes {
			reconnect_interval: "50ms"
			remotes [
				{
					url: "nats://127.0.0.1:%d"
					credentials: '%s'
				}
			]
		}
	`, o.LeafNode.Port, credsFile))
	leaf, _ = RunServerWithConfig(lconf)
	defer leaf.Shutdown()

	time.Sleep(125 * time.Millisecond)
	checkLeafNodeConnectedCount(t, leaf, 0)
	checkLog()
	leaf.Shutdown()

	s.Shutdown()
	drainLog()
}
