package auth

import (
	"testing"

	"github.com/cli/go-gh/v2/pkg/config"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestTokenForHost(t *testing.T) {
	tests := []struct {
		name                  string
		host                  string
		githubToken           string
		githubEnterpriseToken string
		ghToken               string
		ghEnterpriseToken     string
		config                *config.Config
		wantToken             string
		wantSource            string
	}{
		{
			name:       "given there is no env token and no config token, when we get the token for github.com, then it returns the empty string and default source",
			host:       "github.com",
			config:     testNoHostsConfig(),
			wantToken:  "",
			wantSource: defaultSource,
		},
		{
			name:       "given there is no env token and no config token, when we get the token for an enterprise server host, then it returns the empty string and default source",
			host:       "enterprise.com",
			config:     testNoHostsConfig(),
			wantToken:  "",
			wantSource: defaultSource,
		},
		{
			name:        "given GH_TOKEN and GITHUB_TOKEN and a config token are set, when we get the token for github.com, then it returns GH_TOKEN as the priority",
			host:        "github.com",
			ghToken:     "GH_TOKEN",
			githubToken: "GITHUB_TOKEN",
			config:      testHostsConfig(),
			wantToken:   "GH_TOKEN",
			wantSource:  ghToken,
		},
		{
			name:        "given GITHUB_TOKEN and a config token are set, when we get the token for github.com, then it returns GITHUB_TOKEN as the priority",
			host:        "github.com",
			githubToken: "GITHUB_TOKEN",
			config:      testHostsConfig(),
			wantToken:   "GITHUB_TOKEN",
			wantSource:  githubToken,
		},
		{
			name:       "given a config token is set for github.com, when we get the token, then it returns that token and oauth_token source",
			host:       "github.com",
			config:     testHostsConfig(),
			wantToken:  "xxxxxxxxxxxxxxxxxxxx",
			wantSource: oauthToken,
		},
		{
			name:        "given GH_TOKEN and GITHUB_TOKEN and a config token are set, when we get the token for any subdomain of ghe.com, then it returns GH_TOKEN as the priority",
			host:        "tenant.ghe.com",
			ghToken:     "GH_TOKEN",
			githubToken: "GITHUB_TOKEN",
			config:      testHostsConfig(),
			wantToken:   "GH_TOKEN",
			wantSource:  ghToken,
		},
		{
			name:        "given GITHUB_TOKEN and a config token are set, when we get the token for any subdomain of ghe.com, then it returns GITHUB_TOKEN as the priority",
			host:        "tenant.ghe.com",
			githubToken: "GITHUB_TOKEN",
			config:      testHostsConfig(),
			wantToken:   "GITHUB_TOKEN",
			wantSource:  githubToken,
		},
		{
			name:       "given a config token is set for a subdomain of ghe.com, when we get the token for that subdomain, then it returns that token and oauth_token source",
			host:       "tenant.ghe.com",
			config:     testHostsConfig(),
			wantToken:  "zzzzzzzzzzzzzzzzzzzz",
			wantSource: oauthToken,
		},
		{
			name:        "given GH_TOKEN and GITHUB_TOKEN and a config token are set, when we get the token for github.localhost, then it returns GH_TOKEN as the priority",
			host:        "github.localhost",
			ghToken:     "GH_TOKEN",
			githubToken: "GITHUB_TOKEN",
			config:      testHostsConfig(),
			wantToken:   "GH_TOKEN",
			wantSource:  ghToken,
		},
		{
			name:        "given GITHUB_TOKEN and a config token are set, when we get the token for any subdomain of github.localhost, then it returns GITHUB_TOKEN as the priority",
			host:        "github.localhost",
			githubToken: "GITHUB_TOKEN",
			config:      testHostsConfig(),
			wantToken:   "GITHUB_TOKEN",
			wantSource:  githubToken,
		},
		{
			name:                  "given GH_ENTERPRISE_TOKEN and GITHUB_ENTERPRISE_TOKEN and a config token are set, when we get the token for an enterprise server host, then it returns GH_ENTERPRISE_TOKEN as the priority",
			host:                  "enterprise.com",
			ghEnterpriseToken:     "GH_ENTERPRISE_TOKEN",
			githubEnterpriseToken: "GITHUB_ENTERPRISE_TOKEN",
			config:                testHostsConfig(),
			wantToken:             "GH_ENTERPRISE_TOKEN",
			wantSource:            ghEnterpriseToken,
		},
		{
			name:                  "given GITHUB_ENTERPRISE_TOKEN and a config token are set, when we get the token for an enterprise server host, then it returns GITHUB_ENTERPRISE_TOKEN as the priority",
			host:                  "enterprise.com",
			githubEnterpriseToken: "GITHUB_ENTERPRISE_TOKEN",
			config:                testHostsConfig(),
			wantToken:             "GITHUB_ENTERPRISE_TOKEN",
			wantSource:            githubEnterpriseToken,
		},
		{
			name:       "given a config token is set for an enterprise server host, when we get the token for that host, then it returns that token and oauth_token source",
			host:       "enterprise.com",
			config:     testHostsConfig(),
			wantToken:  "yyyyyyyyyyyyyyyyyyyy",
			wantSource: oauthToken,
		},
		{
			name:        "given GH_TOKEN or GITHUB_TOKEN are set, when I get the token for any host not owned by GitHub, we do not get those tokens",
			host:        "unknown.com",
			config:      testNoHostsConfig(),
			ghToken:     "GH_TOKEN",
			githubToken: "GITHUB_TOKEN",
			wantToken:   "",
			wantSource:  defaultSource,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Setenv("GITHUB_TOKEN", tt.githubToken)
			t.Setenv("GITHUB_ENTERPRISE_TOKEN", tt.githubEnterpriseToken)
			t.Setenv("GH_TOKEN", tt.ghToken)
			t.Setenv("GH_ENTERPRISE_TOKEN", tt.ghEnterpriseToken)
			token, source := tokenForHost(tt.config, tt.host)
			require.Equal(t, tt.wantToken, token, "Expected token for \"%s\" to be \"%s\", got \"%s\"", tt.host, tt.wantToken, token)
			require.Equal(t, tt.wantSource, source, "Expected source for \"%s\" to be \"%s\", got \"%s\"", tt.host, tt.wantSource, source)
		})
	}
}

func TestDefaultHost(t *testing.T) {
	tests := []struct {
		name         string
		config       *config.Config
		ghHost       string
		wantHost     string
		wantSource   string
		wantNotFound bool
	}{
		{
			name:       "GH_HOST if set",
			config:     testHostsConfig(),
			ghHost:     "test.com",
			wantHost:   "test.com",
			wantSource: "GH_HOST",
		},
		{
			name:       "authenticated host if only one",
			config:     testSingleHostConfig(),
			wantHost:   "enterprise.com",
			wantSource: "hosts",
		},
		{
			name:         "default host if more than one authenticated host",
			config:       testHostsConfig(),
			wantHost:     "github.com",
			wantSource:   "default",
			wantNotFound: true,
		},
		{
			name:         "default host if no authenticated host",
			config:       testNoHostsConfig(),
			wantHost:     "github.com",
			wantSource:   "default",
			wantNotFound: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.ghHost != "" {
				t.Setenv("GH_HOST", tt.ghHost)
			}
			host, source := defaultHost(tt.config)
			assert.Equal(t, tt.wantHost, host)
			assert.Equal(t, tt.wantSource, source)
		})
	}
}

func TestKnownHosts(t *testing.T) {
	tests := []struct {
		name      string
		config    *config.Config
		ghHost    string
		ghToken   string
		wantHosts []string
	}{
		{
			name:      "no known hosts",
			config:    testNoHostsConfig(),
			wantHosts: []string{},
		},
		{
			name:      "includes GH_HOST",
			config:    testNoHostsConfig(),
			ghHost:    "test.com",
			wantHosts: []string{"test.com"},
		},
		{
			name:      "includes authenticated hosts",
			config:    testHostsConfig(),
			wantHosts: []string{"github.com", "enterprise.com", "tenant.ghe.com"},
		},
		{
			name:      "includes default host if environment auth token",
			config:    testNoHostsConfig(),
			ghToken:   "TOKEN",
			wantHosts: []string{"github.com"},
		},
		{
			name:      "deduplicates hosts",
			config:    testHostsConfig(),
			ghHost:    "test.com",
			ghToken:   "TOKEN",
			wantHosts: []string{"test.com", "github.com", "enterprise.com", "tenant.ghe.com"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.ghHost != "" {
				t.Setenv("GH_HOST", tt.ghHost)
			}
			if tt.ghToken != "" {
				t.Setenv("GH_TOKEN", tt.ghToken)
			}
			hosts := knownHosts(tt.config)
			assert.Equal(t, tt.wantHosts, hosts)
		})
	}
}

func TestIsEnterprise(t *testing.T) {
	tests := []struct {
		name    string
		host    string
		wantOut bool
	}{
		{
			name:    "github",
			host:    "github.com",
			wantOut: false,
		},
		{
			name:    "github API",
			host:    "api.github.com",
			wantOut: false,
		},
		{
			name:    "localhost",
			host:    "github.localhost",
			wantOut: false,
		},
		{
			name:    "localhost API",
			host:    "api.github.localhost",
			wantOut: false,
		},
		{
			name:    "enterprise",
			host:    "mygithub.com",
			wantOut: true,
		},
		{
			name:    "tenant",
			host:    "tenant.ghe.com",
			wantOut: false,
		},
		{
			name:    "tenant API",
			host:    "api.tenant.ghe.com",
			wantOut: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			out := IsEnterprise(tt.host)
			assert.Equal(t, tt.wantOut, out)
		})
	}
}

func TestIsTenancy(t *testing.T) {
	tests := []struct {
		name    string
		host    string
		wantOut bool
	}{
		{
			name:    "github",
			host:    "github.com",
			wantOut: false,
		},
		{
			name:    "github API",
			host:    "api.github.com",
			wantOut: false,
		},
		{
			name:    "localhost",
			host:    "github.localhost",
			wantOut: false,
		},
		{
			name:    "localhost API",
			host:    "api.github.localhost",
			wantOut: false,
		},
		{
			name:    "enterprise",
			host:    "mygithub.com",
			wantOut: false,
		},
		{
			name:    "tenant",
			host:    "tenant.ghe.com",
			wantOut: true,
		},
		{
			name:    "tenant API",
			host:    "api.tenant.ghe.com",
			wantOut: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			out := IsTenancy(tt.host)
			assert.Equal(t, tt.wantOut, out)
		})
	}
}

func TestNormalizeHostname(t *testing.T) {
	tests := []struct {
		name     string
		host     string
		wantHost string
	}{
		{
			name:     "github domain",
			host:     "test.github.com",
			wantHost: "github.com",
		},
		{
			name:     "capitalized",
			host:     "GitHub.com",
			wantHost: "github.com",
		},
		{
			name:     "localhost domain",
			host:     "test.github.localhost",
			wantHost: "github.localhost",
		},
		{
			name:     "enterprise domain",
			host:     "mygithub.com",
			wantHost: "mygithub.com",
		},
		{
			name:     "bare tenant",
			host:     "tenant.ghe.com",
			wantHost: "tenant.ghe.com",
		},
		{
			name:     "subdomained tenant",
			host:     "api.tenant.ghe.com",
			wantHost: "tenant.ghe.com",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			normalized := NormalizeHostname(tt.host)
			assert.Equal(t, tt.wantHost, normalized)
		})
	}
}

func testNoHostsConfig() *config.Config {
	var data = ``
	return config.ReadFromString(data)
}

func testSingleHostConfig() *config.Config {
	var data = `
hosts:
  enterprise.com:
    user: user2
    oauth_token: yyyyyyyyyyyyyyyyyyyy
    git_protocol: https
`
	return config.ReadFromString(data)
}

func testHostsConfig() *config.Config {
	var data = `
hosts:
  github.com:
    user: user1
    oauth_token: xxxxxxxxxxxxxxxxxxxx
    git_protocol: ssh
  enterprise.com:
    user: user2
    oauth_token: yyyyyyyyyyyyyyyyyyyy
    git_protocol: https
  tenant.ghe.com:
    user: user3
    oauth_token: zzzzzzzzzzzzzzzzzzzz
    git_protocol: https
`
	return config.ReadFromString(data)
}