package integration

import (
	"bytes"
	"crypto/tls"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strings"
	"testing"

	knet "k8s.io/kubernetes/pkg/util/net"

	configapi "github.com/openshift/origin/pkg/cmd/server/api"
	testutil "github.com/openshift/origin/test/util"
	testserver "github.com/openshift/origin/test/util/server"
)

func TestWebConsoleExtensions(t *testing.T) {
	// Create a temporary directory.
	tmpDir, err := ioutil.TempDir("", "extensions")
	if err != nil {
		t.Fatalf("Could not create tmp dir for extensions: %v", err)
		return
	}
	defer os.RemoveAll(tmpDir)

	// Create extension files.
	var testData = map[string]string{
		"script1.js":                       script1,
		"script2.js":                       script2,
		"stylesheet1.css":                  stylesheet1,
		"stylesheet2.css":                  stylesheet2,
		"extension1/index.html":            index,
		"extension2/index.html":            index,
		"extension1/files/shakespeare.txt": plaintext,
	}

	for path, content := range testData {
		if err := os.MkdirAll(filepath.Dir(filepath.Join(tmpDir, path)), 0755); err != nil {
			t.Fatalf("Failed creating directory for %s: %v", path, err)
			return
		}
		if err := ioutil.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0755); err != nil {
			t.Fatalf("Failed creating file %s: %v", path, err)
			return
		}
	}

	// Build master config.
	testutil.RequireEtcd(t)
	defer testutil.DumpEtcdOnFailure(t)
	masterOptions, err := testserver.DefaultMasterOptions()
	if err != nil {
		t.Fatalf("Failed creating master configuration: %v", err)
		return
	}
	masterOptions.AssetConfig.ExtensionScripts = []string{
		filepath.Join(tmpDir, "script1.js"),
		filepath.Join(tmpDir, "script2.js"),
	}
	masterOptions.AssetConfig.ExtensionStylesheets = []string{
		filepath.Join(tmpDir, "stylesheet1.css"),
		filepath.Join(tmpDir, "stylesheet2.css"),
	}
	masterOptions.AssetConfig.Extensions = []configapi.AssetExtensionsConfig{
		{
			Name:            "extension1",
			SourceDirectory: filepath.Join(tmpDir, "extension1"),
			HTML5Mode:       true,
		},
		{
			Name:            "extension2",
			SourceDirectory: filepath.Join(tmpDir, "extension2"),
			HTML5Mode:       false,
		},
	}

	// Start server.
	_, err = testserver.StartConfiguredMaster(masterOptions)
	if err != nil {
		t.Fatalf("Unexpected error starting server: %v", err)
		return
	}

	// Inject the base into index.html to test HTML5Mode
	publicURL, err := url.Parse(masterOptions.AssetConfig.PublicURL)
	if err != nil {
		t.Fatalf("Unexpected error parsing PublicURL %q: %v", masterOptions.AssetConfig.PublicURL, err)
		return
	}
	baseInjected := injectBase(index, "extension1", publicURL)

	// TODO: Add tests for caching.

	testcases := map[string]struct {
		URL              string
		Status           int
		Type             string
		Content          []byte
		RedirectLocation string
	}{
		"extension scripts": {
			URL:     "scripts/extensions.js",
			Status:  http.StatusOK,
			Type:    "text/javascript",
			Content: []byte(script1 + ";\n" + script2 + ";\n"),
		},
		"extension css": {
			URL:     "styles/extensions.css",
			Status:  http.StatusOK,
			Type:    "text/css",
			Content: []byte(stylesheet1 + "\n" + stylesheet2 + "\n"),
		},
		"extension index.html (html5Mode on)": {
			URL:     "extensions/extension1/",
			Status:  http.StatusOK,
			Type:    "text/html",
			Content: baseInjected,
		},
		"extension index.html (html5Mode off)": {
			URL:     "extensions/extension2/",
			Status:  http.StatusOK,
			Type:    "text/html",
			Content: []byte(index),
		},
		"extension no trailing slash (html5Mode on)": {
			URL:              "extensions/extension1",
			Status:           http.StatusMovedPermanently,
			RedirectLocation: "extensions/extension1/",
		},
		"extension missing file (html5Mode on)": {
			URL:     "extensions/extension1/does-not-exist/",
			Status:  http.StatusOK,
			Type:    "text/html",
			Content: baseInjected,
		},
		"extension missing file (html5Mode on, no trailing slash)": {
			URL:     "extensions/extension1/does-not-exist",
			Status:  http.StatusOK,
			Type:    "text/html",
			Content: baseInjected,
		},
		"extension missing file (html5Mode off)": {
			URL:    "extensions/extension2/does-not-exist/",
			Status: http.StatusNotFound,
		},
		"extension missing file (html5Mode off, no trailing slash)": {
			URL:    "extensions/extension2/does-not-exist",
			Status: http.StatusNotFound,
		},
		"extension file in subdir": {
			URL:     "extensions/extension1/files/shakespeare.txt",
			Status:  http.StatusOK,
			Type:    "text/plain",
			Content: []byte(plaintext),
		},
		"extension file from other extension": {
			URL:    "extensions/extension2/files/shakespeare.txt",
			Status: http.StatusNotFound,
		},
	}

	transport := knet.SetTransportDefaults(&http.Transport{
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: true,
		},
	})

	for k, tc := range testcases {
		testURL := masterOptions.AssetConfig.PublicURL + tc.URL
		req, err := http.NewRequest("GET", testURL, nil)
		if err != nil {
			t.Errorf("%s: Unexpected error creating request for %q: %v", k, testURL, err)
			continue
		}

		resp, err := transport.RoundTrip(req)
		if err != nil {
			t.Errorf("%s: Unexpected error while accessing %q: %v", k, testURL, err)
			continue
		}

		if resp.StatusCode != tc.Status {
			t.Errorf("%s: Expected status %d for %q, got %d", k, tc.Status, testURL, resp.StatusCode)
			continue
		}

		if resp.StatusCode == http.StatusOK {
			actualType := strings.Split(resp.Header.Get("Content-Type"), ";")[0]
			if actualType != tc.Type {
				t.Errorf("%s: Expected type %q for %q, got %s", k, tc.Type, testURL, actualType)
				continue
			}

			defer resp.Body.Close()
			actualContent, err := ioutil.ReadAll(resp.Body)
			if err != nil {
				t.Errorf("%s: Unexpected error while reading body of %q: %v", k, testURL, err)
				continue
			}
			if !bytes.Equal(actualContent, []byte(tc.Content)) {
				t.Errorf("%s: Response body for %q did not match expected, actual:\n%s\nexpected:\n%s", k, testURL, actualContent, tc.Content)
				continue
			}
		}

		if len(tc.RedirectLocation) > 0 {
			actualLocation := resp.Header.Get("Location")
			expectedLocation := publicURL.Path + tc.RedirectLocation
			if actualLocation != expectedLocation {
				t.Errorf("%s: Expected response header Location %q for %q, got %q", k, expectedLocation, testURL, actualLocation)
				continue
			}
		}
	}
}

func injectBase(content, extensionName string, publicURL *url.URL) []byte {
	base := path.Join(publicURL.Path, "extensions", extensionName) + "/"
	return bytes.Replace([]byte(content), []byte(`<base href="/">`), []byte(fmt.Sprintf(`<base href="%s">`, base)), 1)
}

const (
	script1 = `$(document).ready(function(){$("body").hide().fadeIn(1000);})`

	script2 = `$(document).ready(function() {
	$('#openshift-logo img').attr('src', 'http://example.com/images/my-logo.png');
})`

	stylesheet1 = `html {
	font-family: Gill Sans Extrabold, sans-serif;
	font-size: 14px;
}
`

	stylesheet2 = `.navbar-header {
	background-color: red;
	border-bottom: 1px solid white;
}

.btn-primary {
	background-color: green;
	background-image: none;
}
`

	index = `<!DOCTYPE html>
<html>
<head>
	<title>Hello</title>
	<base href="/">
</head>
<body>
	<h1>Hello</h1>
	<p>Hello, OpenShift!</p>
</body>
</html>
`

	plaintext = `Conscience does make cowards of us all, and thus the native hue of resolution is sicklied o'er with the pale cast of thought.
`
)