package auth

import (
	"encoding/base64"
	"encoding/json"
	"net/http"
	"os"
	"testing"

	"github.com/containers/image/v5/pkg/docker/config"
	"github.com/containers/image/v5/types"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const largeAuthFile = `{"auths":{
	"docker.io/vendor": {"auth": "ZG9ja2VyOnZlbmRvcg=="},
	"https://index.docker.io/v1": {"auth": "ZG9ja2VyOnRvcA=="},
	"quay.io/libpod": {"auth": "cXVheTpsaWJwb2Q="},
	"quay.io": {"auth": "cXVheTp0b3A="}
}}`

// Semantics of largeAuthFile
var largeAuthFileValues = map[string]types.DockerAuthConfig{
	"docker.io/vendor": {Username: "docker", Password: "vendor"},
	"docker.io":        {Username: "docker", Password: "top"},
	"quay.io/libpod":   {Username: "quay", Password: "libpod"},
	"quay.io":          {Username: "quay", Password: "top"},
}

// systemContextForAuthFile returns a types.SystemContext with AuthFilePath pointing
// to a temporary file with fileContents, or nil if fileContents is empty; and a cleanup
// function the caller must arrange to call.
func systemContextForAuthFile(t *testing.T, fileContents string) (*types.SystemContext, func()) {
	if fileContents == "" {
		return nil, func() {}
	}

	f, err := os.CreateTemp("", "auth.json")
	require.NoError(t, err)
	path := f.Name()
	err = os.WriteFile(path, []byte(fileContents), 0700)
	require.NoError(t, err)
	return &types.SystemContext{AuthFilePath: path}, func() { os.Remove(path) }
}

// Test that GetCredentials() correctly parses what MakeXRegistryConfigHeader() produces
func TestMakeXRegistryConfigHeaderGetCredentialsRoundtrip(t *testing.T) {
	for _, tc := range []struct {
		name               string
		fileContents       string
		username, password string
		expectedOverride   *types.DockerAuthConfig
		expectedFileValues map[string]types.DockerAuthConfig
	}{
		{
			name:               "no data",
			fileContents:       "",
			username:           "",
			password:           "",
			expectedOverride:   nil,
			expectedFileValues: nil,
		},
		{
			name:               "file data",
			fileContents:       largeAuthFile,
			username:           "",
			password:           "",
			expectedOverride:   nil,
			expectedFileValues: largeAuthFileValues,
		},
		{
			name:               "file data + override",
			fileContents:       largeAuthFile,
			username:           "override-user",
			password:           "override-pass",
			expectedOverride:   &types.DockerAuthConfig{Username: "override-user", Password: "override-pass"},
			expectedFileValues: largeAuthFileValues,
		},
	} {
		sys, cleanup := systemContextForAuthFile(t, tc.fileContents)
		defer cleanup()
		headers, err := MakeXRegistryConfigHeader(sys, tc.username, tc.password)
		require.NoError(t, err)
		req, err := http.NewRequest(http.MethodPost, "/", nil)
		require.NoError(t, err, tc.name)
		for _, v := range headers.Values(xRegistryConfigHeader) {
			req.Header.Add(xRegistryConfigHeader, v)
		}

		override, resPath, err := GetCredentials(req)
		require.NoError(t, err, tc.name)
		defer RemoveAuthfile(resPath)
		if tc.expectedOverride == nil {
			assert.Nil(t, override, tc.name)
		} else {
			require.NotNil(t, override, tc.name)
			assert.Equal(t, *tc.expectedOverride, *override, tc.name)
		}
		for key, expectedAuth := range tc.expectedFileValues {
			auth, err := config.GetCredentials(&types.SystemContext{AuthFilePath: resPath}, key)
			require.NoError(t, err, tc.name)
			assert.Equal(t, expectedAuth, auth, "%s, key %s", tc.name, key)
		}
	}
}

// Test that GetCredentials() correctly parses what MakeXRegistryAuthHeader() produces
func TestMakeXRegistryAuthHeaderGetCredentialsRoundtrip(t *testing.T) {
	for _, tc := range []struct {
		name               string
		fileContents       string
		username, password string
		expectedOverride   *types.DockerAuthConfig
		expectedFileValues map[string]types.DockerAuthConfig
	}{
		{
			name:               "override",
			fileContents:       "",
			username:           "override-user",
			password:           "override-pass",
			expectedOverride:   &types.DockerAuthConfig{Username: "override-user", Password: "override-pass"},
			expectedFileValues: nil,
		},
		{
			name:               "file data",
			fileContents:       largeAuthFile,
			username:           "",
			password:           "",
			expectedFileValues: largeAuthFileValues,
		},
	} {
		sys, cleanup := systemContextForAuthFile(t, tc.fileContents)
		defer cleanup()
		headers, err := MakeXRegistryAuthHeader(sys, tc.username, tc.password)
		require.NoError(t, err)
		req, err := http.NewRequest(http.MethodPost, "/", nil)
		require.NoError(t, err, tc.name)
		for _, v := range headers.Values(xRegistryAuthHeader) {
			req.Header.Set(xRegistryAuthHeader, v)
		}

		override, resPath, err := GetCredentials(req)
		require.NoError(t, err, tc.name)
		defer RemoveAuthfile(resPath)
		if tc.expectedOverride == nil {
			assert.Nil(t, override, tc.name)
		} else {
			require.NotNil(t, override, tc.name)
			assert.Equal(t, *tc.expectedOverride, *override, tc.name)
		}
		for key, expectedAuth := range tc.expectedFileValues {
			auth, err := config.GetCredentials(&types.SystemContext{AuthFilePath: resPath}, key)
			require.NoError(t, err, tc.name)
			assert.Equal(t, expectedAuth, auth, "%s, key %s", tc.name, key)
		}
	}
}

func TestMakeXRegistryConfigHeader(t *testing.T) {
	for _, tc := range []struct {
		name               string
		fileContents       string
		username, password string
		shouldErr          bool
		expectedContents   string
	}{
		{
			name:             "no data",
			fileContents:     "",
			username:         "",
			password:         "",
			expectedContents: "",
		},
		{
			name:         "invalid JSON",
			fileContents: "@invalid JSON",
			username:     "",
			password:     "",
			shouldErr:    true,
		},
		{
			name:         "file data",
			fileContents: largeAuthFile,
			username:     "",
			password:     "",
			expectedContents: `{
			"docker.io/vendor": {"username": "docker", "password": "vendor"},
			"docker.io": {"username": "docker", "password": "top"},
			"quay.io/libpod": {"username": "quay", "password": "libpod"},
			"quay.io": {"username": "quay", "password": "top"}
			}`,
		},
		{
			name:         "file data + override",
			fileContents: largeAuthFile,
			username:     "override-user",
			password:     "override-pass",
			expectedContents: `{
				"docker.io/vendor": {"username": "docker", "password": "vendor"},
				"docker.io": {"username": "docker", "password": "top"},
				"quay.io/libpod": {"username": "quay", "password": "libpod"},
				"quay.io": {"username": "quay", "password": "top"},
				"": {"username": "override-user", "password": "override-pass"}
				}`,
		},
	} {
		sys, cleanup := systemContextForAuthFile(t, tc.fileContents)
		defer cleanup()
		res, err := MakeXRegistryConfigHeader(sys, tc.username, tc.password)
		if tc.shouldErr {
			assert.Error(t, err, tc.name)
		} else {
			require.NoError(t, err, tc.name)
			if tc.expectedContents == "" {
				assert.Empty(t, res, tc.name)
			} else {
				require.Len(t, res, 1, tc.name)
				header, ok := res[xRegistryConfigHeader]
				require.True(t, ok, tc.name)
				decodedHeader, err := base64.URLEncoding.DecodeString(header[0])
				require.NoError(t, err, tc.name)
				// Don't test for a specific JSON representation, just for the expected contents.
				expected := map[string]interface{}{}
				actual := map[string]interface{}{}
				err = json.Unmarshal([]byte(tc.expectedContents), &expected)
				require.NoError(t, err, tc.name)
				err = json.Unmarshal(decodedHeader, &actual)
				require.NoError(t, err, tc.name)
				assert.Equal(t, expected, actual, tc.name)
			}
		}
	}
}

func TestMakeXRegistryAuthHeader(t *testing.T) {
	for _, tc := range []struct {
		name               string
		fileContents       string
		username, password string
		shouldErr          bool
		expectedContents   string
	}{
		{
			name:             "override",
			fileContents:     "",
			username:         "override-user",
			password:         "override-pass",
			expectedContents: `{"username": "override-user", "password": "override-pass"}`,
		},
		{
			name:         "invalid JSON",
			fileContents: "@invalid JSON",
			username:     "",
			password:     "",
			shouldErr:    true,
		},
		{
			name:         "file data",
			fileContents: largeAuthFile,
			username:     "",
			password:     "",
			expectedContents: `{
			"docker.io/vendor": {"username": "docker", "password": "vendor"},
			"docker.io": {"username": "docker", "password": "top"},
			"quay.io/libpod": {"username": "quay", "password": "libpod"},
			"quay.io": {"username": "quay", "password": "top"}
			}`,
		},
	} {
		sys, cleanup := systemContextForAuthFile(t, tc.fileContents)
		defer cleanup()
		res, err := MakeXRegistryAuthHeader(sys, tc.username, tc.password)
		if tc.shouldErr {
			assert.Error(t, err, tc.name)
		} else {
			require.NoError(t, err, tc.name)
			if tc.expectedContents == "" {
				assert.Empty(t, res, tc.name)
			} else {
				require.Len(t, res, 1, tc.name)
				header, ok := res[xRegistryAuthHeader]
				require.True(t, ok, tc.name)
				decodedHeader, err := base64.URLEncoding.DecodeString(header[0])
				require.NoError(t, err, tc.name)
				// Don't test for a specific JSON representation, just for the expected contents.
				expected := map[string]interface{}{}
				actual := map[string]interface{}{}
				err = json.Unmarshal([]byte(tc.expectedContents), &expected)
				require.NoError(t, err, tc.name)
				err = json.Unmarshal(decodedHeader, &actual)
				require.NoError(t, err, tc.name)
				assert.Equal(t, expected, actual, tc.name)
			}
		}
	}
}

func TestAuthConfigsToAuthFile(t *testing.T) {
	for _, tc := range []struct {
		name             string
		server           string
		shouldErr        bool
		expectedContains string
	}{
		{
			name:             "empty auth configs",
			server:           "",
			shouldErr:        false,
			expectedContains: "{}",
		},
		{
			name:             "registry with a namespace prefix",
			server:           "my-registry.local/username",
			shouldErr:        false,
			expectedContains: `"my-registry.local/username":`,
		},
		{
			name:             "URLs are interpreted as full registries",
			server:           "http://my-registry.local/username",
			shouldErr:        false,
			expectedContains: `"my-registry.local":`,
		},
		{
			name:             "the old-style docker registry URL is normalized",
			server:           "http://index.docker.io/v1/",
			shouldErr:        false,
			expectedContains: `"docker.io":`,
		},
		{
			name:             "docker.io vendor namespace",
			server:           "docker.io/vendor",
			shouldErr:        false,
			expectedContains: `"docker.io/vendor":`,
		},
	} {
		configs := map[string]types.DockerAuthConfig{}
		if tc.server != "" {
			configs[tc.server] = types.DockerAuthConfig{}
		}

		filePath, err := authConfigsToAuthFile(configs)

		if tc.shouldErr {
			assert.Error(t, err)
			assert.Empty(t, filePath)
		} else {
			assert.NoError(t, err)
			content, err := os.ReadFile(filePath)
			require.NoError(t, err)
			assert.Contains(t, string(content), tc.expectedContains)
			os.Remove(filePath)
		}
	}
}

func TestParseSingleAuthHeader(t *testing.T) {
	for _, tc := range []struct {
		input     string
		shouldErr bool
		expected  types.DockerAuthConfig
	}{
		{
			input:    "", // An empty (or missing) header
			expected: types.DockerAuthConfig{},
		},
		{
			input:    "null",
			expected: types.DockerAuthConfig{},
		},
		// Invalid JSON
		{input: "@", shouldErr: true},
		// Success
		{
			input:    base64.URLEncoding.EncodeToString([]byte(`{"username":"u1","password":"p1"}`)),
			expected: types.DockerAuthConfig{Username: "u1", Password: "p1"},
		},
	} {
		res, err := parseSingleAuthHeader(tc.input)
		if tc.shouldErr {
			assert.Error(t, err, tc.input)
		} else {
			require.NoError(t, err, tc.input)
			assert.Equal(t, tc.expected, res, tc.input)
		}
	}
}

func TestParseMultiAuthHeader(t *testing.T) {
	for _, tc := range []struct {
		input     string
		shouldErr bool
		expected  map[string]types.DockerAuthConfig
	}{
		// Empty header
		{input: "", expected: nil},
		// "null"
		{input: "null", expected: nil},
		// Invalid JSON
		{input: "@", shouldErr: true},
		// Success
		{
			input: base64.URLEncoding.EncodeToString([]byte(
				`{"https://index.docker.io/v1/":{"username":"u1","password":"p1"},` +
					`"quay.io/libpod":{"username":"u2","password":"p2"}}`)),
			expected: map[string]types.DockerAuthConfig{
				"https://index.docker.io/v1/": {Username: "u1", Password: "p1"},
				"quay.io/libpod":              {Username: "u2", Password: "p2"},
			},
		},
	} {
		res, err := parseMultiAuthHeader(tc.input)
		if tc.shouldErr {
			assert.Error(t, err, tc.input)
		} else {
			require.NoError(t, err, tc.input)
			assert.Equal(t, tc.expected, res, tc.input)
		}
	}
}
