// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

//go:build linux

package tshttpproxy

import (
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"tailscale.com/tstest"
)

func TestSynologyProxyFromConfigCached(t *testing.T) {
	req, err := http.NewRequest("GET", "http://example.org/", nil)
	if err != nil {
		t.Fatal(err)
	}

	tstest.Replace(t, &synologyProxyConfigPath, filepath.Join(t.TempDir(), "proxy.conf"))

	t.Run("no-config-file", func(t *testing.T) {
		if _, err := os.Stat(synologyProxyConfigPath); err == nil {
			t.Fatalf("%s must not exist for this test", synologyProxyConfigPath)
		}

		cache.updated = time.Time{}
		cache.httpProxy = nil
		cache.httpsProxy = nil

		if val, err := synologyProxyFromConfigCached(req); val != nil || err != nil {
			t.Fatalf("got %s, %v; want nil, nil", val, err)
		}

		if got, want := cache.updated.UTC(), time.Unix(0, 0).UTC(); !got.Equal(want) {
			t.Fatalf("got %s, want %s", got, want)
		}
		if cache.httpProxy != nil {
			t.Fatalf("got %s, want nil", cache.httpProxy)
		}
		if cache.httpsProxy != nil {
			t.Fatalf("got %s, want nil", cache.httpsProxy)
		}
	})

	t.Run("config-file-updated", func(t *testing.T) {
		cache.updated = time.Now()
		cache.httpProxy = nil
		cache.httpsProxy = nil

		if err := os.WriteFile(synologyProxyConfigPath, []byte(`
proxy_enabled=yes
http_host=10.0.0.55
http_port=80
https_host=10.0.0.66
https_port=443
		`), 0600); err != nil {
			t.Fatal(err)
		}

		val, err := synologyProxyFromConfigCached(req)
		if err != nil {
			t.Fatal(err)
		}

		if cache.httpProxy == nil {
			t.Fatal("http proxy was not cached")
		}
		if cache.httpsProxy == nil {
			t.Fatal("https proxy was not cached")
		}

		if want := urlMustParse("http://10.0.0.55:80"); val.String() != want.String() {
			t.Fatalf("got %s; want %s", val, want)
		}
	})

	t.Run("config-file-removed", func(t *testing.T) {
		cache.updated = time.Now()
		cache.httpProxy = urlMustParse("http://127.0.0.1/")
		cache.httpsProxy = urlMustParse("http://127.0.0.1/")

		if err := os.Remove(synologyProxyConfigPath); err != nil && !os.IsNotExist(err) {
			t.Fatal(err)
		}

		val, err := synologyProxyFromConfigCached(req)
		if err != nil {
			t.Fatal(err)
		}
		if val != nil {
			t.Fatalf("got %s; want nil", val)
		}
		if cache.httpProxy != nil {
			t.Fatalf("got %s, want nil", cache.httpProxy)
		}
		if cache.httpsProxy != nil {
			t.Fatalf("got %s, want nil", cache.httpsProxy)
		}
	})

	t.Run("picks-proxy-from-request-scheme", func(t *testing.T) {
		cache.updated = time.Now()
		cache.httpProxy = nil
		cache.httpsProxy = nil

		if err := os.WriteFile(synologyProxyConfigPath, []byte(`
proxy_enabled=yes
http_host=10.0.0.55
http_port=80
https_host=10.0.0.66
https_port=443
		`), 0600); err != nil {
			t.Fatal(err)
		}

		httpReq, err := http.NewRequest("GET", "http://example.com", nil)
		if err != nil {
			t.Fatal(err)
		}
		val, err := synologyProxyFromConfigCached(httpReq)
		if err != nil {
			t.Fatal(err)
		}
		if val == nil {
			t.Fatalf("got nil, want an http URL")
		}
		if got, want := val.String(), "http://10.0.0.55:80"; got != want {
			t.Fatalf("got %q, want %q", got, want)
		}

		httpsReq, err := http.NewRequest("GET", "https://example.com", nil)
		if err != nil {
			t.Fatal(err)
		}
		val, err = synologyProxyFromConfigCached(httpsReq)
		if err != nil {
			t.Fatal(err)
		}
		if val == nil {
			t.Fatalf("got nil, want an http URL")
		}
		if got, want := val.String(), "http://10.0.0.66:443"; got != want {
			t.Fatalf("got %q, want %q", got, want)
		}
	})
}

func TestSynologyProxiesFromConfig(t *testing.T) {
	var (
		openReader io.ReadCloser
		openErr    error
	)
	tstest.Replace(t, &openSynologyProxyConf, func() (io.ReadCloser, error) {
		return openReader, openErr
	})

	t.Run("with-config", func(t *testing.T) {
		mc := &mustCloser{Reader: strings.NewReader(`
proxy_user=foo
proxy_pwd=bar
proxy_enabled=yes
adv_enabled=yes
bypass_enabled=yes
auth_enabled=yes
https_host=10.0.0.66
https_port=8443
http_host=10.0.0.55
http_port=80
	`)}
		defer mc.check(t)
		openReader = mc

		httpProxy, httpsProxy, err := synologyProxiesFromConfig()

		if got, want := err, openErr; got != want {
			t.Fatalf("got %s, want %s", got, want)
		}

		if got, want := httpsProxy, urlMustParse("http://foo:bar@10.0.0.66:8443"); got.String() != want.String() {
			t.Fatalf("got %s, want %s", got, want)
		}

		if got, want := err, openErr; got != want {
			t.Fatalf("got %s, want %s", got, want)
		}

		if got, want := httpProxy, urlMustParse("http://foo:bar@10.0.0.55:80"); got.String() != want.String() {
			t.Fatalf("got %s, want %s", got, want)
		}

	})

	t.Run("nonexistent-config", func(t *testing.T) {
		openReader = nil
		openErr = os.ErrNotExist

		httpProxy, httpsProxy, err := synologyProxiesFromConfig()
		if err != nil {
			t.Fatalf("expected no error, got %s", err)
		}
		if httpProxy != nil {
			t.Fatalf("expected no url, got %s", httpProxy)
		}
		if httpsProxy != nil {
			t.Fatalf("expected no url, got %s", httpsProxy)
		}
	})

	t.Run("error-opening-config", func(t *testing.T) {
		openReader = nil
		openErr = errors.New("example error")

		httpProxy, httpsProxy, err := synologyProxiesFromConfig()
		if err != openErr {
			t.Fatalf("expected %s, got %s", openErr, err)
		}
		if httpProxy != nil {
			t.Fatalf("expected no url, got %s", httpProxy)
		}
		if httpsProxy != nil {
			t.Fatalf("expected no url, got %s", httpsProxy)
		}
	})

}

func TestParseSynologyConfig(t *testing.T) {
	cases := map[string]struct {
		input      string
		httpProxy  *url.URL
		httpsProxy *url.URL
		err        error
	}{
		"populated": {
			input: `
proxy_user=foo
proxy_pwd=bar
proxy_enabled=yes
adv_enabled=yes
bypass_enabled=yes
auth_enabled=yes
https_host=10.0.0.66
https_port=8443
http_host=10.0.0.55
http_port=80
`,
			httpProxy:  urlMustParse("http://foo:bar@10.0.0.55:80"),
			httpsProxy: urlMustParse("http://foo:bar@10.0.0.66:8443"),
			err:        nil,
		},
		"no-auth": {
			input: `
proxy_user=foo
proxy_pwd=bar
proxy_enabled=yes
adv_enabled=yes
bypass_enabled=yes
auth_enabled=no
https_host=10.0.0.66
https_port=8443
http_host=10.0.0.55
http_port=80
`,
			httpProxy:  urlMustParse("http://10.0.0.55:80"),
			httpsProxy: urlMustParse("http://10.0.0.66:8443"),
			err:        nil,
		},
		"http-only": {
			input: `
proxy_user=foo
proxy_pwd=bar
proxy_enabled=yes
adv_enabled=yes
bypass_enabled=yes
auth_enabled=yes
https_host=
https_port=8443
http_host=10.0.0.55
http_port=80
`,
			httpProxy:  urlMustParse("http://foo:bar@10.0.0.55:80"),
			httpsProxy: nil,
			err:        nil,
		},
		"empty": {
			input: `
proxy_user=
proxy_pwd=
proxy_enabled=
adv_enabled=
bypass_enabled=
auth_enabled=
https_host=
https_port=
http_host=
http_port=
`,
			httpProxy:  nil,
			httpsProxy: nil,
			err:        nil,
		},
	}

	for name, example := range cases {
		t.Run(name, func(t *testing.T) {
			httpProxy, httpsProxy, err := parseSynologyConfig(strings.NewReader(example.input))
			if err != example.err {
				t.Fatal(err)
			}
			if example.err != nil {
				return
			}

			if example.httpProxy == nil && httpProxy != nil {
				t.Fatalf("got %s, want nil", httpProxy)
			}

			if example.httpProxy != nil {
				if httpProxy == nil {
					t.Fatalf("got nil, want %s", example.httpProxy)
				}

				if got, want := example.httpProxy.String(), httpProxy.String(); got != want {
					t.Fatalf("got %s, want %s", got, want)
				}
			}

			if example.httpsProxy == nil && httpsProxy != nil {
				t.Fatalf("got %s, want nil", httpProxy)
			}

			if example.httpsProxy != nil {
				if httpsProxy == nil {
					t.Fatalf("got nil, want %s", example.httpsProxy)
				}

				if got, want := example.httpsProxy.String(), httpsProxy.String(); got != want {
					t.Fatalf("got %s, want %s", got, want)
				}
			}
		})
	}
}
func urlMustParse(u string) *url.URL {
	r, err := url.Parse(u)
	if err != nil {
		panic(fmt.Sprintf("urlMustParse: %s", err))
	}
	return r
}

type mustCloser struct {
	io.Reader
	closed bool
}

func (m *mustCloser) Close() error {
	m.closed = true
	return nil
}

func (m *mustCloser) check(t *testing.T) {
	if !m.closed {
		t.Errorf("mustCloser wrapping %#v was not closed at time of check", m.Reader)
	}
}
