package main

import (
	"os"
	"testing"

	"github.com/ghodss/yaml"
	"github.com/kylelemons/godebug/pretty"

	"github.com/dexidp/dex/connector/mock"
	"github.com/dexidp/dex/connector/oidc"
	"github.com/dexidp/dex/server"
	"github.com/dexidp/dex/storage"
	"github.com/dexidp/dex/storage/sql"
)

var _ = yaml.YAMLToJSON

func TestValidConfiguration(t *testing.T) {
	configuration := Config{
		Issuer: "http://127.0.0.1:5556/dex",
		Storage: Storage{
			Type: "sqlite3",
			Config: &sql.SQLite3{
				File: "examples/dex.db",
			},
		},
		Web: Web{
			HTTP: "127.0.0.1:5556",
		},
		StaticConnectors: []Connector{
			{
				Type:   "mockCallback",
				ID:     "mock",
				Name:   "Example",
				Config: &mock.CallbackConfig{},
			},
		},
	}
	if err := configuration.Validate(); err != nil {
		t.Fatalf("this configuration should have been valid: %v", err)
	}
}

func TestInvalidConfiguration(t *testing.T) {
	configuration := Config{}
	err := configuration.Validate()
	if err == nil {
		t.Fatal("this configuration should be invalid")
	}
	got := err.Error()
	wanted := `invalid Config:
	-	no issuer specified in config file
	-	no storage supplied in config file
	-	must supply a HTTP/HTTPS  address to listen on`
	if got != wanted {
		t.Fatalf("Expected error message to be %q, got %q", wanted, got)
	}
}

func TestUnmarshalConfig(t *testing.T) {
	rawConfig := []byte(`
issuer: http://127.0.0.1:5556/dex
storage:
  type: postgres
  config:
    host: 10.0.0.1
    port: 65432
    maxOpenConns: 5
    maxIdleConns: 3
    connMaxLifetime: 30
    connectionTimeout: 3
web:
  http: 127.0.0.1:5556

frontend:
  dir: ./web
  extra:
    foo: bar

staticClients:
- id: example-app
  redirectURIs:
  - 'http://127.0.0.1:5555/callback'
  name: 'Example App'
  secret: ZXhhbXBsZS1hcHAtc2VjcmV0

oauth2:
  alwaysShowLoginScreen: true

connectors:
- type: mockCallback
  id: mock
  name: Example
- type: oidc
  id: google
  name: Google
  config:
    issuer: https://accounts.google.com
    clientID: foo
    clientSecret: bar
    redirectURI: http://127.0.0.1:5556/dex/callback/google

enablePasswordDB: true
staticPasswords:
- email: "admin@example.com"
  # bcrypt hash of the string "password"
  hash: "$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy"
  username: "admin"
  userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
- email: "foo@example.com"
  # base64'd value of the same bcrypt hash above. We want to be able to parse both of these
  hash: "JDJhJDEwJDMzRU1UMGNWWVZsUHk2V0FNQ0xzY2VMWWpXaHVIcGJ6NXl1Wnh1L0dBRmowM0o5THl0anV5"
  username: "foo"
  userID: "41331323-6f44-45e6-b3b9-2c4b60c02be5"

expiry:
  signingKeys: "7h"
  idTokens: "25h"
  authRequests: "25h"
  deviceRequests: "10m"

logger:
  level: "debug"
  format: "json"
`)

	want := Config{
		Issuer: "http://127.0.0.1:5556/dex",
		Storage: Storage{
			Type: "postgres",
			Config: &sql.Postgres{
				NetworkDB: sql.NetworkDB{
					Host:              "10.0.0.1",
					Port:              65432,
					MaxOpenConns:      5,
					MaxIdleConns:      3,
					ConnMaxLifetime:   30,
					ConnectionTimeout: 3,
				},
			},
		},
		Web: Web{
			HTTP: "127.0.0.1:5556",
		},
		Frontend: server.WebConfig{
			Dir: "./web",
			Extra: map[string]string{
				"foo": "bar",
			},
		},
		StaticClients: []storage.Client{
			{
				ID:     "example-app",
				Secret: "ZXhhbXBsZS1hcHAtc2VjcmV0",
				Name:   "Example App",
				RedirectURIs: []string{
					"http://127.0.0.1:5555/callback",
				},
			},
		},
		OAuth2: OAuth2{
			AlwaysShowLoginScreen: true,
		},
		StaticConnectors: []Connector{
			{
				Type:   "mockCallback",
				ID:     "mock",
				Name:   "Example",
				Config: &mock.CallbackConfig{},
			},
			{
				Type: "oidc",
				ID:   "google",
				Name: "Google",
				Config: &oidc.Config{
					Issuer:       "https://accounts.google.com",
					ClientID:     "foo",
					ClientSecret: "bar",
					RedirectURI:  "http://127.0.0.1:5556/dex/callback/google",
				},
			},
		},
		EnablePasswordDB: true,
		StaticPasswords: []password{
			{
				Email:    "admin@example.com",
				Hash:     []byte("$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy"),
				Username: "admin",
				UserID:   "08a8684b-db88-4b73-90a9-3cd1661f5466",
			},
			{
				Email:    "foo@example.com",
				Hash:     []byte("$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy"),
				Username: "foo",
				UserID:   "41331323-6f44-45e6-b3b9-2c4b60c02be5",
			},
		},
		Expiry: Expiry{
			SigningKeys:    "7h",
			IDTokens:       "25h",
			AuthRequests:   "25h",
			DeviceRequests: "10m",
		},
		Logger: Logger{
			Level:  "debug",
			Format: "json",
		},
	}

	var c Config
	if err := yaml.Unmarshal(rawConfig, &c); err != nil {
		t.Fatalf("failed to decode config: %v", err)
	}
	if diff := pretty.Compare(c, want); diff != "" {
		t.Errorf("got!=want: %s", diff)
	}
}

func TestUnmarshalConfigWithEnvNoExpand(t *testing.T) {
	// If the env variable DEX_EXPAND_ENV is set and has a "falsy" value, os.ExpandEnv is disabled.
	// ParseBool: "It accepts 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False."
	checkUnmarshalConfigWithEnv(t, "0", false)
	checkUnmarshalConfigWithEnv(t, "f", false)
	checkUnmarshalConfigWithEnv(t, "F", false)
	checkUnmarshalConfigWithEnv(t, "FALSE", false)
	checkUnmarshalConfigWithEnv(t, "false", false)
	checkUnmarshalConfigWithEnv(t, "False", false)
	os.Unsetenv("DEX_EXPAND_ENV")
}

func TestUnmarshalConfigWithEnvExpand(t *testing.T) {
	// If the env variable DEX_EXPAND_ENV is unset or has a "truthy" or unknown value, os.ExpandEnv is enabled.
	// ParseBool: "It accepts 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False."
	checkUnmarshalConfigWithEnv(t, "1", true)
	checkUnmarshalConfigWithEnv(t, "t", true)
	checkUnmarshalConfigWithEnv(t, "T", true)
	checkUnmarshalConfigWithEnv(t, "TRUE", true)
	checkUnmarshalConfigWithEnv(t, "true", true)
	checkUnmarshalConfigWithEnv(t, "True", true)
	// Values that can't be parsed as bool:
	checkUnmarshalConfigWithEnv(t, "UNSET", true)
	checkUnmarshalConfigWithEnv(t, "", true)
	checkUnmarshalConfigWithEnv(t, "whatever - true is default", true)
	os.Unsetenv("DEX_EXPAND_ENV")
}

func checkUnmarshalConfigWithEnv(t *testing.T, dexExpandEnv string, wantExpandEnv bool) {
	// For hashFromEnv:
	os.Setenv("DEX_FOO_USER_PASSWORD", "$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy")
	// For os.ExpandEnv ($VAR -> value_of_VAR):
	os.Setenv("DEX_FOO_POSTGRES_HOST", "10.0.0.1")
	os.Setenv("DEX_FOO_OIDC_CLIENT_SECRET", "bar")
	if dexExpandEnv != "UNSET" {
		os.Setenv("DEX_EXPAND_ENV", dexExpandEnv)
	} else {
		os.Unsetenv("DEX_EXPAND_ENV")
	}

	rawConfig := []byte(`
issuer: http://127.0.0.1:5556/dex
storage:
  type: postgres
  config:
    # Env variables are expanded in raw YAML source.
    # Single quotes work fine, as long as the env variable doesn't contain any.
    host: '$DEX_FOO_POSTGRES_HOST'
    port: 65432
    maxOpenConns: 5
    maxIdleConns: 3
    connMaxLifetime: 30
    connectionTimeout: 3
web:
  http: 127.0.0.1:5556

frontend:
  dir: ./web
  extra:
    foo: bar

staticClients:
- id: example-app
  redirectURIs:
  - 'http://127.0.0.1:5555/callback'
  name: 'Example App'
  secret: ZXhhbXBsZS1hcHAtc2VjcmV0

oauth2:
  alwaysShowLoginScreen: true

connectors:
- type: mockCallback
  id: mock
  name: Example
- type: oidc
  id: google
  name: Google
  config:
    issuer: https://accounts.google.com
    clientID: foo
    # Env variables are expanded in raw YAML source.
    # Single quotes work fine, as long as the env variable doesn't contain any.
    clientSecret: '$DEX_FOO_OIDC_CLIENT_SECRET'
    redirectURI: http://127.0.0.1:5556/dex/callback/google

enablePasswordDB: true
staticPasswords:
- email: "admin@example.com"
  # bcrypt hash of the string "password"
  hash: "$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy"
  username: "admin"
  userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
- email: "foo@example.com"
  hashFromEnv: "DEX_FOO_USER_PASSWORD"
  username: "foo"
  userID: "41331323-6f44-45e6-b3b9-2c4b60c02be5"

expiry:
  signingKeys: "7h"
  idTokens: "25h"
  authRequests: "25h"

logger:
  level: "debug"
  format: "json"
`)

	// This is not a valid hostname. It's only used to check whether os.ExpandEnv was applied or not.
	wantPostgresHost := "$DEX_FOO_POSTGRES_HOST"
	wantOidcClientSecret := "$DEX_FOO_OIDC_CLIENT_SECRET"
	if wantExpandEnv {
		wantPostgresHost = "10.0.0.1"
		wantOidcClientSecret = "bar"
	}

	want := Config{
		Issuer: "http://127.0.0.1:5556/dex",
		Storage: Storage{
			Type: "postgres",
			Config: &sql.Postgres{
				NetworkDB: sql.NetworkDB{
					Host:              wantPostgresHost,
					Port:              65432,
					MaxOpenConns:      5,
					MaxIdleConns:      3,
					ConnMaxLifetime:   30,
					ConnectionTimeout: 3,
				},
			},
		},
		Web: Web{
			HTTP: "127.0.0.1:5556",
		},
		Frontend: server.WebConfig{
			Dir: "./web",
			Extra: map[string]string{
				"foo": "bar",
			},
		},
		StaticClients: []storage.Client{
			{
				ID:     "example-app",
				Secret: "ZXhhbXBsZS1hcHAtc2VjcmV0",
				Name:   "Example App",
				RedirectURIs: []string{
					"http://127.0.0.1:5555/callback",
				},
			},
		},
		OAuth2: OAuth2{
			AlwaysShowLoginScreen: true,
		},
		StaticConnectors: []Connector{
			{
				Type:   "mockCallback",
				ID:     "mock",
				Name:   "Example",
				Config: &mock.CallbackConfig{},
			},
			{
				Type: "oidc",
				ID:   "google",
				Name: "Google",
				Config: &oidc.Config{
					Issuer:       "https://accounts.google.com",
					ClientID:     "foo",
					ClientSecret: wantOidcClientSecret,
					RedirectURI:  "http://127.0.0.1:5556/dex/callback/google",
				},
			},
		},
		EnablePasswordDB: true,
		StaticPasswords: []password{
			{
				Email:    "admin@example.com",
				Hash:     []byte("$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy"),
				Username: "admin",
				UserID:   "08a8684b-db88-4b73-90a9-3cd1661f5466",
			},
			{
				Email:    "foo@example.com",
				Hash:     []byte("$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy"),
				Username: "foo",
				UserID:   "41331323-6f44-45e6-b3b9-2c4b60c02be5",
			},
		},
		Expiry: Expiry{
			SigningKeys:  "7h",
			IDTokens:     "25h",
			AuthRequests: "25h",
		},
		Logger: Logger{
			Level:  "debug",
			Format: "json",
		},
	}

	var c Config
	if err := yaml.Unmarshal(rawConfig, &c); err != nil {
		t.Fatalf("failed to decode config: %v", err)
	}
	if diff := pretty.Compare(c, want); diff != "" {
		t.Errorf("got!=want: %s", diff)
	}
}
