// Copyright 2018 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 (
	"errors"
	"fmt"
	"os"
	"strings"
	"testing"
)

func TestConfigCheck(t *testing.T) {
	tests := []struct {
		// name is the name of the test.
		name string

		// config is content of the configuration file.
		config string

		// warningErr is an error that does not prevent server from starting.
		warningErr error

		// errorLine is the location of the error.
		errorLine int

		// errorPos is the position of the error.
		errorPos int

		// warning errors also include a reason optionally.
		reason string

		// newDefaultErr is a configuration error that includes source of error.
		err error
	}{
		{
			name: "when unknown field is used at top level",
			config: `
                monitor = "127.0.0.1:4442"
                `,
			err:       errors.New(`unknown field "monitor"`),
			errorLine: 2,
			errorPos:  17,
		},
		{
			name: "when default permissions are used at top level",
			config: `
                "default_permissions" {
                  publish = ["_SANDBOX.>"]
                  subscribe = ["_SANDBOX.>"]
                }
                `,
			err:       errors.New(`unknown field "default_permissions"`),
			errorLine: 2,
			errorPos:  18,
		},
		{
			name: "when authorization config is empty",
			config: `
		authorization = {
		}
		`,
			err: nil,
		},
		{
			name: "when authorization config has unknown fields",
			config: `
		authorization = {
		  foo = "bar"
		}
		`,
			err:       errors.New(`unknown field "foo"`),
			errorLine: 3,
			errorPos:  5,
		},
		{
			name: "when authorization config has unknown fields",
			config: `
		port = 4222

		authorization = {
		  user = "hello"
		  foo = "bar"
		  password = "world"
		}

		`,
			err:       errors.New(`unknown field "foo"`),
			errorLine: 6,
			errorPos:  5,
		},
		{
			name: "when user authorization config has unknown fields",
			config: `
		authorization = {
		  users = [
		    {
		      user = "foo"
		      pass = "bar"
		      token = "quux"
		    }
		  ]
		}
		`,
			err:       errors.New(`unknown field "token"`),
			errorLine: 7,
			errorPos:  9,
		},
		{
			name: "when user authorization permissions config has unknown fields",
			config: `
		authorization {
		  permissions {
		    subscribe = {}
		    inboxes = {}
		    publish = {}
		  }
		}
		`,
			err:       errors.New(`Unknown field "inboxes" parsing permissions`),
			errorLine: 5,
			errorPos:  7,
		},
		{
			name: "when user authorization permissions config has unknown fields within allow or deny",
			config: `
		authorization {
		  permissions {
		    subscribe = {
		      allow = ["hello", "world"]
		      deny = ["foo", "bar"]
		      denied = "_INBOX.>"
		    }
		    publish = {}
		  }
		}
		`,
			err:       errors.New(`Unknown field name "denied" parsing subject permissions, only 'allow' or 'deny' are permitted`),
			errorLine: 7,
			errorPos:  9,
		},
		{
			name: "when user authorization permissions config has unknown fields within allow or deny",
			config: `
		authorization {
		  permissions {
		    publish = {
		      allow = ["hello", "world"]
		      deny = ["foo", "bar"]
		      allowed = "_INBOX.>"
		    }
		    subscribe = {}
		  }
		}
		`,
			err:       errors.New(`Unknown field name "allowed" parsing subject permissions, only 'allow' or 'deny' are permitted`),
			errorLine: 7,
			errorPos:  9,
		},
		{
			name: "when user authorization permissions config has unknown fields using arrays",
			config: `
		authorization {

		 default_permissions {
		   subscribe = ["a"]
		   publish = ["b"]
		   inboxes = ["c"]
		 }

		 users = [
		   {
		     user = "foo"
		     pass = "bar"
		   }
		  ]
		}
		`,
			err:       errors.New(`Unknown field "inboxes" parsing permissions`),
			errorLine: 7,
			errorPos:  6,
		},
		{
			name: "when user authorization permissions config has unknown fields using strings",
			config: `
		authorization {

		 default_permissions {
		   subscribe = "a"
		   requests = "b"
		   publish = "c"
		 }

		 users = [
		   {
		     user = "foo"
		     pass = "bar"
		   }
		  ]
		}
		`,
			err:       errors.New(`Unknown field "requests" parsing permissions`),
			errorLine: 6,
			errorPos:  6,
		},
		{
			name: "when user authorization permissions config is empty",
			config: `
		authorization = {
		  users = [
		    {
		      user = "foo", pass = "bar", permissions = {
		      }
		    }
		  ]
		}
		`,
			err: nil,
		},
		{
			name: "when unknown permissions are included in user config",
			config: `
		authorization = {
		  users = [
		    {
		      user = "foo", pass = "bar", permissions {
		        inboxes = true
		      }
		    }
		  ]
		}
		`,
			err:       errors.New(`Unknown field "inboxes" parsing permissions`),
			errorLine: 6,
			errorPos:  11,
		},
		{
			name: "when clustering config is empty",
			config: `
		cluster = {
		}
		`,

			err: nil,
		},
		{
			name: "when unknown option is in clustering config",
			config: `
		# NATS Server Configuration
		port = 4222

		cluster = {

		  port = 6222

		  foo = "bar"

		  authorization {
		    user = "hello"
		    pass = "world"
		  }

		}
		`,

			err:       errors.New(`unknown field "foo"`),
			errorLine: 9,
			errorPos:  5,
		},
		{
			name: "when unknown option is in clustering authorization config",
			config: `
		cluster = {
		  authorization {
		    foo = "bar"
		  }
		}
		`,

			err:       errors.New(`unknown field "foo"`),
			errorLine: 4,
			errorPos:  7,
		},
		{
			name: "when unknown option is in tls config",
			config: `
		tls = {
		  hello = "world"
		}
		`,
			err:       errors.New(`error parsing tls config, unknown field ["hello"]`),
			errorLine: 3,
			errorPos:  5,
		},
		{
			name: "when unknown option is in cluster tls config",
			config: `
		cluster {
		  tls = {
		    foo = "bar"
		  }
		}
		`,
			err:       errors.New(`error parsing tls config, unknown field ["foo"]`),
			errorLine: 4,
			errorPos:  7,
		},
		{
			name: "when using cipher suites in the TLS config",
			config: `
		tls = {
		    cipher_suites: [
			"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
			"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
		    ]
		    preferences = []
		}
		`,
			err:       errors.New(`error parsing tls config, unknown field ["preferences"]`),
			errorLine: 7,
			errorPos:  7,
		},
		{
			name: "when using curve preferences in the TLS config",
			config: `
		tls = {
		    curve_preferences: [
			"CurveP256",
			"CurveP384",
			"CurveP521"
		    ]
		    suites = []
		}
		`,
			err:       errors.New(`error parsing tls config, unknown field ["suites"]`),
			errorLine: 8,
			errorPos:  7,
		},
		{
			name: "when using curve preferences in the TLS config",
			config: `
		tls = {
		    curve_preferences: [
			"CurveP5210000"
		    ]
		}
		`,
			err:       errors.New(`Unrecognized curve preference CurveP5210000`),
			errorLine: 4,
			errorPos:  5,
		},
		{
			name: "when unknown option is in cluster config with defined routes",
			config: `
		cluster {
		  port = 6222
		  routes = [
		    nats://127.0.0.1:6222
		  ]
		  peers = []
		}
		`,
			err:       errors.New(`unknown field "peers"`),
			errorLine: 7,
			errorPos:  5,
		},
		{
			name: "when used as variable in authorization block it should not be considered as unknown field",
			config: `
		# listen:   127.0.0.1:-1
		listen:   127.0.0.1:4222

		authorization {
		  # Superuser can do anything.
		  super_user = {
		    publish = ">"
		    subscribe = ">"
		  }

		  # Can do requests on foo or bar, and subscribe to anything
		  # that is a response to an _INBOX.
		  #
		  # Notice that authorization filters can be singletons or arrays.
		  req_pub_user = {
		    publish = ["req.foo", "req.bar"]
		    subscribe = "_INBOX.>"
		  }

		  # Setup a default user that can subscribe to anything, but has
		  # no publish capabilities.
		  default_user = {
		    subscribe = "PUBLIC.>"
		  }

		  unused = "hello"

		  # Default permissions if none presented. e.g. susan below.
		  default_permissions: $default_user

		  # Users listed with persmissions.
		  users = [
		    {user: alice, password: foo, permissions: $super_user}
		    {user: bob,   password: bar, permissions: $req_pub_user}
		    {user: susan, password: baz}
		  ]
		}
		`,
			err:       errors.New(`unknown field "unused"`),
			errorLine: 27,
			errorPos:  5,
		},
		{
			name: "when used as variable in top level config it should not be considered as unknown field",
			config: `
		monitoring_port = 8222

		http_port = $monitoring_port

		port = 4222
		`,
			err: nil,
		},
		{
			name: "when used as variable in cluster config it should not be considered as unknown field",
			config: `
		cluster {
		  clustering_port = 6222
		  port = $clustering_port
		}
		`,
			err: nil,
		},
		{
			name: "when setting permissions within cluster authorization block",
			config: `
		cluster {
		  authorization {
		    permissions = {
		      publish = { allow = ["foo", "bar"] }
		    }
		  }

		  permissions = {
		    publish = { deny = ["foo", "bar"] }
		  }
		}
		`,
			warningErr: errors.New(`invalid use of field "authorization"`),
			errorLine:  3,
			errorPos:   5,
			reason:     `setting "permissions" within cluster authorization block is deprecated`,
		},
		{
			name: "when write deadline is used with deprecated usage",
			config: `
                write_deadline = 100
		`,
			warningErr: errors.New(`invalid use of field "write_deadline"`),
			errorLine:  2,
			errorPos:   17,
			reason:     `write_deadline should be converted to a duration`,
		},
		/////////////////////
		// ACCOUNTS	   //
		/////////////////////
		{
			name: "when accounts block is correctly configured",
			config: `
		http_port = 8222

		accounts {

		  #
		  # synadia > nats.io, cncf
		  #
		  synadia {
		    # SAADJL5XAEM6BDYSWDTGVILJVY54CQXZM5ZLG4FRUAKB62HWRTPNSGXOHA
		    nkey = "AC5GRL36RQV7MJ2GT6WQSCKDKJKYTK4T2LGLWJ2SEJKRDHFOQQWGGFQL"

		    users [
		      {
		        # SUAEL6RU3BSDAFKOHNTEOK5Q6FTM5FTAMWVIKBET6FHPO4JRII3CYELVNM
		        nkey = "UCARKS2E3KVB7YORL2DG34XLT7PUCOL2SVM7YXV6ETHLW6Z46UUJ2VZ3"
		      }
		    ]

		    exports = [
		      { service: "synadia.requests", accounts: [nats, cncf] }
		    ]
		  }

		  #
		  # nats < synadia
		  #
		  nats {
		    # SUAJTM55JH4BNYDA22DMDZJSRBRKVDGSLYK2HDIOCM3LPWCDXIDV5Q4CIE
		    nkey = "ADRZ42QBM7SXQDXXTSVWT2WLLFYOQGAFC4TO6WOAXHEKQHIXR4HFYJDS"

		    users [
		      {
		        # SUADZTYQAKTY5NQM7XRB5XR3C24M6ROGZLBZ6P5HJJSSOFUGC5YXOOECOM
		        nkey = "UD6AYQSOIN2IN5OGC6VQZCR4H3UFMIOXSW6NNS6N53CLJA4PB56CEJJI"
		      }
		    ]

		    imports = [
		      # This account has to send requests to 'nats.requests' subject
		      { service: { account: "synadia", subject: "synadia.requests" }, to: "nats.requests" }
		    ]
		  }

		  #
		  # cncf < synadia
		  #
		  cncf {
		    # SAAFHDZX7SGZ2SWHPS22JRPPK5WX44NPLNXQHR5C5RIF6QRI3U65VFY6C4
		    nkey = "AD4YRVUJF2KASKPGRMNXTYKIYSCB3IHHB4Y2ME6B2PDIV5QJ23C2ZRIT"

		    users [
		      {
		        # SUAKINP3Z2BPUXWOFSW2FZC7TFJCMMU7DHKP2C62IJQUDASOCDSTDTRMJQ
		        nkey = "UB57IEMPG4KOTPFV5A66QKE2HZ3XBXFHVRCCVMJEWKECMVN2HSH3VTSJ"
		      }
		    ]

		    imports = [
		      # This account has to send requests to 'synadia.requests' subject
		      { service: { account: "synadia", subject: "synadia.requests" } }
		    ]
		  }
		}
				`,
			err: nil,
		},
		{
			name: "when nkey is invalid within accounts block",
			config: `
		accounts {

		  #
		  # synadia > nats.io, cncf
		  #
		  synadia {
		    # SAADJL5XAEM6BDYSWDTGVILJVY54CQXZM5ZLG4FRUAKB62HWRTPNSGXOHA
		    nkey = "AC5GRL36RQV7MJ2GT6WQSCKDKJKYTK4T2LGLWJ2SEJKRDHFOQQWGGFQL"

		    users [
		      {
		        # SUAEL6RU3BSDAFKOHNTEOK5Q6FTM5FTAMWVIKBET6FHPO4JRII3CYELVNM
		        nkey = "SCARKS2E3KVB7YORL2DG34XLT7PUCOL2SVM7YXV6ETHLW6Z46UUJ2VZ3"
		      }
		    ]

		    exports = [
		      { service: "synadia.requests", accounts: [nats, cncf] }
		    ]
		  }

		  #
		  # nats < synadia
		  #
		  nats {
		    # SUAJTM55JH4BNYDA22DMDZJSRBRKVDGSLYK2HDIOCM3LPWCDXIDV5Q4CIE
		    nkey = "ADRZ42QBM7SXQDXXTSVWT2WLLFYOQGAFC4TO6WOAXHEKQHIXR4HFYJDS"

		    users [
		      {
		        # SUADZTYQAKTY5NQM7XRB5XR3C24M6ROGZLBZ6P5HJJSSOFUGC5YXOOECOM
		        nkey = "UD6AYQSOIN2IN5OGC6VQZCR4H3UFMIOXSW6NNS6N53CLJA4PB56CEJJI"
		      }
		    ]

		    imports = [
		      # This account has to send requests to 'nats.requests' subject
		      { service: { account: "synadia", subject: "synadia.requests" }, to: "nats.requests" }
		    ]
		  }

		  #
		  # cncf < synadia
		  #
		  cncf {
		    # SAAFHDZX7SGZ2SWHPS22JRPPK5WX44NPLNXQHR5C5RIF6QRI3U65VFY6C4
		    nkey = "AD4YRVUJF2KASKPGRMNXTYKIYSCB3IHHB4Y2ME6B2PDIV5QJ23C2ZRIT"

		    users [
		      {
		        # SUAKINP3Z2BPUXWOFSW2FZC7TFJCMMU7DHKP2C62IJQUDASOCDSTDTRMJQ
		        nkey = "UB57IEMPG4KOTPFV5A66QKE2HZ3XBXFHVRCCVMJEWKECMVN2HSH3VTSJ"
		      }
		    ]

		    imports = [
		      # This account has to send requests to 'synadia.requests' subject
		      { service: { account: "synadia", subject: "synadia.requests" } }
		    ]
		  }
		}
				`,
			err:       errors.New(`Not a valid public nkey for a user`),
			errorLine: 14,
			errorPos:  11,
		},
		{
			name: "when account user within accounts block has no user, pass",
			config: `
		accounts {

		  #
		  # synadia > nats.io, cncf
		  #
		  synadia {
		    # SAADJL5XAEM6BDYSWDTGVILJVY54CQXZM5ZLG4FRUAKB62HWRTPNSGXOHA
		    nkey = "AC5GRL36RQV7MJ2GT6WQSCKDKJKYTK4T2LGLWJ2SEJKRDHFOQQWGGFQL"

		    users [
		      {
		      }
		    ]

		    exports = [
		      { service: "synadia.requests", accounts: [nats, cncf] }
		    ]
		  }
		}
				`,
			err:       errors.New(`User entry requires a user and a password`),
			errorLine: 13,
			errorPos:  10,
		},
		{
			name: "when accounts block has unknown fields",
			config: `
		http_port = 8222

		accounts {
                  foo = "bar"
		}`,
			err:       errors.New(`Expected map entries for accounts`),
			errorLine: 5,
			errorPos:  19,
		},
		{
			name: "when accounts has a referenced config variable within same block",
			config: `
			  accounts {
			    PERMISSIONS = {
				publish = {
				  allow = ["foo","bar"]
				  deny = ["quux"]
				}
			    }

			    synadia {
				nkey = "AC5GRL36RQV7MJ2GT6WQSCKDKJKYTK4T2LGLWJ2SEJKRDHFOQQWGGFQL"

				users [
				  {
				    nkey = "UCARKS2E3KVB7YORL2DG34XLT7PUCOL2SVM7YXV6ETHLW6Z46UUJ2VZ3"
				    permissions = $PERMISSIONS
				  }
				]
				exports = [
				  { stream: "synadia.>" }
				]
			    }
			  }`,
			err: nil,
		},
		{
			name: "when accounts has an unreferenced config variables within same block",
			config: `
			  accounts {
			    PERMISSIONS = {
				publish = {
				  allow = ["foo","bar"]
				  deny = ["quux"]
				}
			    }

			    synadia {
				nkey = "AC5GRL36RQV7MJ2GT6WQSCKDKJKYTK4T2LGLWJ2SEJKRDHFOQQWGGFQL"

				users [
				  {
				    nkey = "UCARKS2E3KVB7YORL2DG34XLT7PUCOL2SVM7YXV6ETHLW6Z46UUJ2VZ3"
				  }
				]
				exports = [
				  { stream: "synadia.>" }
				]
			   }
			 }`,
			err:       errors.New(`unknown field "publish"`),
			errorLine: 4,
			errorPos:  5,
		},
		{
			name: "when accounts block defines a global account",
			config: `
		http_port = 8222

		accounts {
                  $G = {
                  }
		}
				`,
			err:       errors.New(`"$G" is a Reserved Account`),
			errorLine: 5,
			errorPos:  19,
		},
		{
			name: "when accounts block uses an invalid public key",
			config: `
		accounts {
                  synadia = {
                    nkey = "invalid"
                  }
		}
				`,
			err:       errors.New(`Not a valid public nkey for an account: "invalid"`),
			errorLine: 4,
			errorPos:  21,
		},
		{
			name: "when accounts list includes reserved account",
			config: `
                port = 4222

		accounts = [foo, bar, "$G"]

                http_port = 8222
				`,
			err:       errors.New(`"$G" is a Reserved Account`),
			errorLine: 4,
			errorPos:  26,
		},
		{
			name: "when accounts list includes a dupe entry",
			config: `
                port = 4222

		accounts = [foo, bar, bar]

                http_port = 8222
				`,
			err:       errors.New(`Duplicate Account Entry: bar`),
			errorLine: 4,
			errorPos:  25,
		},
		{
			name: "when accounts block includes a dupe user",
			config: `
                port = 4222

		accounts = {
                  nats {
                    users = [
                      { user: "foo",   pass: "bar" },
                      { user: "hello", pass: "world" },
                      { user: "foo",   pass: "bar" }
                    ]
                  }
                }

                http_port = 8222
				`,
			err:       errors.New(`Duplicate user "foo" detected`),
			errorLine: 6,
			errorPos:  21,
		},
		{
			name: "when accounts block imports are not a list",
			config: `
                port = 4222

		accounts = {
                  nats {
                    imports = true
                  }
                }

                http_port = 8222
				`,
			err:       errors.New(`Imports should be an array, got bool`),
			errorLine: 6,
			errorPos:  21,
		},
		{
			name: "when accounts block exports are not a list",
			config: `
                port = 4222

		accounts = {
                  nats {
                    exports = true
                  }
                }

                http_port = 8222
				`,
			err:       errors.New(`Exports should be an array, got bool`),
			errorLine: 6,
			errorPos:  21,
		},
		{
			name: "when accounts block imports items are not a map",
			config: `
                port = 4222

		accounts = {
                  nats {
                    imports = [
                      false
                    ]
                  }
                }

                http_port = 8222
				`,
			err:       errors.New(`Import Items should be a map with type entry, got bool`),
			errorLine: 7,
			errorPos:  23,
		},
		{
			name: "when accounts block export items are not a map",
			config: `
                port = 4222

		accounts = {
                  nats {
                    exports = [
                      false
                    ]
                  }
                }

                http_port = 8222
				`,
			err:       errors.New(`Export Items should be a map with type entry, got bool`),
			errorLine: 7,
			errorPos:  23,
		},
		{
			name: "when accounts exports has a stream name that is not a string",
			config: `
                port = 4222

		accounts = {
                  nats {
                    exports = [
                      {
                        stream: false
                      }
                    ]
                  }
                }

                http_port = 8222
				`,
			err:       errors.New(`Expected stream name to be string, got bool`),
			errorLine: 8,
			errorPos:  25,
		},
		{
			name: "when accounts exports has a service name that is not a string",
			config: `
		accounts = {
                  nats {
                    exports = [
                      {
                        service: false
                      }
                    ]
                  }
                }
				`,
			err:       errors.New(`Expected service name to be string, got bool`),
			errorLine: 6,
			errorPos:  25,
		},
		{
			name: "when accounts imports stream without name",
			config: `
                port = 4222

		accounts = {
                  nats {
                    imports = [
                      { stream: { }}
                    ]
                  }
                }

                http_port = 8222
				`,
			err:       errors.New(`Expect an account name and a subject`),
			errorLine: 7,
			errorPos:  25,
		},
		{
			name: "when accounts imports service without name",
			config: `
                port = 4222

		accounts = {
                  nats {
                    imports = [
                      { service: { }}
                    ]
                  }
                }

                http_port = 8222
				`,
			err:       errors.New(`Expect an account name and a subject`),
			errorLine: 7,
			errorPos:  25,
		},
		{
			name: "when user authorization config has both token and users",
			config: `
		authorization = {
                 token = "s3cr3t"
		  users = [
		    {
		      user = "foo"
		      pass = "bar"
		    }
		  ]
		}
		`,
			err:       errors.New(`Can not have a token and a users array`),
			errorLine: 2,
			errorPos:  3,
		},
		{
			name: "when user authorization config has both token and user",
			config: `
		authorization = {
  	          user = "foo"
		  pass = "bar"
		  users = [
		    {
		      user = "foo"
		      pass = "bar"
		    }
		  ]
		}
		`,
			err:       errors.New(`Can not have a single user/pass and a users array`),
			errorLine: 2,
			errorPos:  3,
		},
		{
			name: "when user authorization config has users not as a list",
			config: `
		authorization = {
		  users = false
		}
		`,
			err:       errors.New(`Expected users field to be an array, got false`),
			errorLine: 3,
			errorPos:  5,
		},
		{
			name: "when user authorization config has users not as a map",
			config: `
		authorization = {
		  users = [false]
		}
		`,
			err:       errors.New(`Expected user entry to be a map/struct, got false`),
			errorLine: 3,
			errorPos:  14,
		},
		{
			name: "when user authorization config has permissions not as a map",
			config: `
		authorization = {
		  users = [{user: hello, pass: world}]
                  permissions = false
		}
		`,
			err:       errors.New(`Expected permissions to be a map/struct, got false`),
			errorLine: 4,
			errorPos:  19,
		},
		{
			name: "when user authorization permissions config has invalid fields within allow",
			config: `
		authorization {
		  permissions {
		    publish = {
		      allow = [false, "hello", "world"]
		      deny = ["foo", "bar"]
		    }
		    subscribe = {}
		  }
		}
		`,
			err:       errors.New(`Subject in permissions array cannot be cast to string`),
			errorLine: 5,
			errorPos:  18,
		},
		{
			name: "when user authorization permissions config has invalid fields within deny",
			config: `
		authorization {
		  permissions {
		    publish = {
		      allow = ["hello", "world"]
		      deny = [true, "foo", "bar"]
		    }
		    subscribe = {}
		  }
		}
		`,
			err:       errors.New(`Subject in permissions array cannot be cast to string`),
			errorLine: 6,
			errorPos:  17,
		},
		{
			name: "when user authorization permissions config has invalid type",
			config: `
		authorization {
		  permissions {
		    publish = {
		      allow = false
		    }
		    subscribe = {}
		  }
		}
		`,
			err:       errors.New(`Expected subject permissions to be a subject, or array of subjects, got bool`),
			errorLine: 5,
			errorPos:  9,
		},
		{
			name: "when user authorization permissions subject is invalid",
			config: `
		authorization {
		  permissions {
		    publish = {
		      allow = ["foo..bar"]
		    }
		    subscribe = {}
		  }
		}
		`,
			err:       errors.New(`Subject "foo..bar" is not a valid subject`),
			errorLine: 5,
			errorPos:  9,
		},
		{
			name: "when cluster config listen is invalid",
			config: `
		cluster {
		  listen = "0.0.0.0:XXXX"
		}
		`,
			err:       errors.New(`Could not parse port "XXXX"`),
			errorLine: 3,
			errorPos:  5,
		},
		{
			name: "when cluster config includes multiple users",
			config: `
		cluster {
		  authorization {
                    users = []
                  }
		}
		`,
			err:       errors.New(`Cluster authorization does not allow multiple users`),
			errorLine: 3,
			errorPos:  5,
		},
		{
			name: "when cluster routes are invalid",
			config: `
		cluster {
                  routes = [
                    "0.0.0.0:XXXX"
                    # "0.0.0.0:YYYY"
                    # "0.0.0.0:ZZZZ"
                  ]
		}
		`,
			err:       errors.New(`error parsing route url ["0.0.0.0:XXXX"]`),
			errorLine: 4,
			errorPos:  22,
		},
		{
			name: "when setting invalid TLS config within cluster block",
			config: `
		cluster {
		  tls {
		  }
		}
		`,
			err:       errors.New(`error parsing X509 certificate/key pair: open : no such file or directory`),
			errorLine: 3,
			errorPos:  5,
		},
		{
			name: "invalid lame_duck_duration type",
			config: `
		lame_duck_duration: abc
			`,
			err:       errors.New(`error parsing lame_duck_duration: time: invalid duration abc`),
			errorLine: 2,
			errorPos:  3,
		},
	}

	checkConfig := func(config string) error {
		opts := &Options{
			CheckConfig: true,
		}
		return opts.ProcessConfigFile(config)
	}

	checkErr := func(t *testing.T, err, expectedErr error) {
		t.Helper()
		switch {
		case err == nil && expectedErr == nil:
			// OK
		case err != nil && expectedErr == nil:
			t.Errorf("Unexpected error after processing config: %s", err)
		case err == nil && expectedErr != nil:
			t.Errorf("Expected %q error after processing invalid config but got nothing", expectedErr)
		}
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			conf := createConfFile(t, []byte(test.config))
			defer os.Remove(conf)
			err := checkConfig(conf)
			var expectedErr error

			// Check for either warnings or errors.
			if test.err != nil {
				expectedErr = test.err
			} else if test.warningErr != nil {
				expectedErr = test.warningErr
			}

			if err != nil && expectedErr != nil {
				msg := fmt.Sprintf("%s:%d:%d: %s", conf, test.errorLine, test.errorPos, expectedErr.Error())
				if test.reason != "" {
					msg += ": " + test.reason
				}
				msg += "\n"
				if err.Error() != msg {
					t.Errorf("Expected:\n%q\ngot:\n%q", msg, err.Error())
				}
			}

			checkErr(t, err, expectedErr)
		})
	}
}

func TestConfigCheckIncludes(t *testing.T) {
	// Check happy path first.
	opts := &Options{
		CheckConfig: true,
	}
	err := opts.ProcessConfigFile("./configs/include_conf_check_a.conf")
	if err != nil {
		t.Errorf("Unexpected error processing include files with configuration check enabled: %v", err)
	}

	opts = &Options{
		CheckConfig: true,
	}
	err = opts.ProcessConfigFile("./configs/include_bad_conf_check_a.conf")
	if err == nil {
		t.Errorf("Expected error processing include files with configuration check enabled: %v", err)
	}
	expectedErr := errors.New(`configs/include_bad_conf_check_b.conf:10:19: unknown field "monitoring_port"` + "\n")
	if err != nil && expectedErr != nil && err.Error() != expectedErr.Error() {
		t.Errorf("Expected: \n%q, got\n: %q", expectedErr.Error(), err.Error())
	}
}

func TestConfigCheckMultipleErrors(t *testing.T) {
	opts := &Options{
		CheckConfig: true,
	}
	err := opts.ProcessConfigFile("./configs/multiple_errors.conf")
	if err == nil {
		t.Errorf("Expected error processing config files with multiple errors check enabled: %v", err)
	}
	cerr, ok := err.(*processConfigErr)
	if !ok {
		t.Fatalf("Expected a configuration process error")
	}
	got := len(cerr.Warnings())
	expected := 1
	if got != expected {
		t.Errorf("Expected a %d warning, got: %d", expected, got)
	}
	got = len(cerr.Errors())
	expected = 7
	if got != 7 {
		t.Errorf("Expected a %d errors, got: %d", expected, got)
	}

	errMsg := err.Error()

	errs := []string{
		`./configs/multiple_errors.conf:12:1: invalid use of field "write_deadline": write_deadline should be converted to a duration`,
		`./configs/multiple_errors.conf:2:1: Cannot have a user/pass and token`,
		`./configs/multiple_errors.conf:10:1: unknown field "monitoring"`,
		`./configs/multiple_errors.conf:67:3: Cluster authorization does not allow multiple users`,
		`./configs/multiple_errors.conf:21:5: Not a valid public nkey for an account: "OC5GRL36RQV7MJ2GT6WQSCKDKJKYTK4T2LGLWJ2SEJKRDHFOQQWGGFQL"`,
		`./configs/multiple_errors.conf:26:9: Not a valid public nkey for a user`,
		`./configs/multiple_errors.conf:36:5: Not a valid public nkey for an account: "ODRZ42QBM7SXQDXXTSVWT2WLLFYOQGAFC4TO6WOAXHEKQHIXR4HFYJDS"`,
		`./configs/multiple_errors.conf:41:9: Not a valid public nkey for a user`,
	}
	for _, msg := range errs {
		found := strings.Contains(errMsg, msg)
		if !found {
			t.Errorf("Expected to find error %q", msg)
		}
	}
}
