package gpgme

import (
	"bytes"
	"flag"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
)

const (
	testGPGHome    = "./testdata/gpghome"
	testData       = "data\n"
	testCipherText = `-----BEGIN PGP MESSAGE-----
Version: GnuPG v1

hQEMAw4698hF4WUhAQf/SdkbF/zUE6YjBxscDurrUZunSnt87kipLXypSxTDIdgj
O9huAaQwBz4uAJf2DuEN/7iAFGhi/v45NTujrG+7ocfjM3m/A2T80g4RVF5kKXBr
pFFgH7bMRY6VdZt1GKI9izSO/uFkoKXG8M31tCX3hWntQUJ9p+n1avGpu3wo4Ru3
CJhpL+ChDzXuZv4IK81ahrixEz4fJH0vd0TbsHpTXx4WPkTGXelM0R9PwiY7TovZ
onGZUIplrfA1HUUbQfzExyFw3oAo1/almzD5CBIfq5EnU8Siy5BNulDXm0/44h8A
lOUy6xqx7ITnWMYYf4a1cFoW80Yk+x6SYQqbcqHFQdJIAVr00V4pPV4ppFcXgdw/
BxKERzDupyqS0tcfVFCYLRmvtQp7ceOS6jRW3geXPPIz1U/VYBvKlvFu4XTMCS6z
4qY4SzZlFEsU
=IQOA
-----END PGP MESSAGE-----`
	textSignedCipherText = `-----BEGIN PGP MESSAGE-----
Version: GnuPG v1

hQEMAw4698hF4WUhAQf9FaI8zSltIMcDxqGtpcTfcBtC30rcKqE/oa1bacxuZQKW
uT671N7J2TjU8tLIsAO9lDSRHCsf6DbB+O6qOJsu6eJownyMUEmJ2oBON9Z2x+Ci
aS0KeRqcRxNJYbIth9/ffa2I4sSBA0GS93yCfGKHNL4JvNo/dgVjJwOPWZ5TKu49
n9h81LMwTQ8qpLGPeo7nsgBtSKNTi5s7pC0bd9HTii7gQcLUEeeqk5U7oG5CRN48
zKPrVud8pIDvSoYuymZmjWI1MmF4xQ+6IHK7hWKMyA2RkS4CpyBPERMifc+y+K2+
LnKLt2ul9jxgqv3fd/dU7UhyqdvJdj83tUvhN4Iru9LAuQHfVsP23keH8CHpUzCj
PkuUYvpgmRdMSC+l3yBCqnaaAI51rRdyJ+HX4DrrinOFst2JfXEICiQAg5UkO4i/
YYHsfD3xfk7MKMOY/DfV25OyAU6Yeq/5SMisONhtlzcosVU7HULudbIKSG6q/tCx
jRGu+oMIBdiQ+eipnaLVetyGxSEDzxHH367NbxC9ffxK7I+Srmwh+oS/yMNuZA8r
bUp6NbykK8Si7VNgVG1+yOIULLuR0VBO5+e2PKQU8FP0wABTn7h3zE8z4rCrcaZl
43bc/TBUyTdWY79e2P1otaCbhQGAjfF8ClPbAdsFppykf5I2ZDDf8c67d0Nqui74
Yry4/fF/2sAzyoyzdP6ktBw6pCA1MbwDqTGg7aEI8Es3X+jfh3qIaLnGmZ7jIyUl
D4jgGAEpFU+mpPH8eukKuT+OJ3P6gdmSiAJH7+C96tlcEg9BNxjYNkCTil/3yygQ
EV/5zFTEpzm4CtYHHdmY5uCaEJq/4hhE8BY8
=8mxI
-----END PGP MESSAGE-----`
	testSignedText = `-----BEGIN PGP MESSAGE-----
Version: GnuPG v1

owEBQwG8/pANAwACAQMn/7Ain2E2AcsTYgBW47+PVGVzdCBtZXNzYWdlCokBHAQA
AQIABgUCVuO/jwAKCRADJ/+wIp9hNh9lCACPiDkY0CqI9ss4EBcpToqnF/8NmV99
2wi6FmbQnUmY98OMM2VJXrX6PvfD/X+FsiLog0CZU4heMEAI3Dd3qELgTfaTFqNc
bbDkenzA0kO734WLsEU/z1F9iWAcfeF3crKqd3fBw5kZ1PkhuJFdcqQEOUQALvXY
8VAtQmQWzf3sn2KIQ7R1wyAJVoUZaN5Xwc9Y0F1l4Xxifax8nkFBl35X6gmHRxZ7
jlfmWMcAkXASNXl9/Yso2XGJMs85JPhZPJ3KJRuuurnhZSxAbDJMNBFJ+HbQv3y6
pupeR7ut6pWJxr6MND793yoFGoRYwKklQdfP4xzFCatYRU4RkPBp95KJ
=RMUj
-----END PGP MESSAGE-----
`
)

func TestMain(m *testing.M) {
	flag.Parse()
	os.Setenv("GNUPGHOME", absTestGPGHome())
	unsetenvGPGAgentInfo()
	os.Exit(m.Run())
}

func compareEngineInfo(t *testing.T, info *EngineInfo, proto Protocol, fileName, homeDir string) {
	for info != nil && info.Protocol() != proto {
		info = info.Next()
	}
	if info == nil {
		t.Errorf("Expected engine info %d not found", proto)
		return
	}
	if info.FileName() != fileName {
		t.Errorf("Testing file name %s does not match %s", info.FileName(), fileName)
	}
	if info.HomeDir() != homeDir {
		t.Errorf("Testing home directory %s does not match %s", info.HomeDir(), homeDir)
	}
}

func TestEngineInfo(t *testing.T) {
	testProto := ProtocolOpenPGP // Careful, this is global state!
	defer func() {
		_ = SetEngineInfo(testProto, "", "") // Try to reset to defaults after we are done.
	}()

	testFN := "testFN"
	testHomeDir := "testHomeDir"
	checkError(t, SetEngineInfo(testProto, testFN, testHomeDir))

	info, err := GetEngineInfo()
	checkError(t, err)
	compareEngineInfo(t, info, testProto, testFN, testHomeDir)

	// SetEngineInfo with empty strings works, using defaults which we don't know,
	// so just test that it doesn't fail.
	checkError(t, SetEngineInfo(testProto, testFN, ""))
	checkError(t, SetEngineInfo(testProto, "", testHomeDir))
}

func TestGetDirInfo(t *testing.T) {
	info := GetDirInfo("fail")
	if info != "" {
		t.Errorf("expected no info, got: %q", info)
	}

	info = GetDirInfo("homedir")
	if info == "" {
		t.Error("expected dir info, got nothing")
	}
}

func ctxWithCallback(t *testing.T) *Context {
	ensureVersion(t, "1.", "can only set password callback for GPG v1.x")

	ctx, err := New()
	checkError(t, err)

	checkError(t, ctx.SetCallback(func(uid_hint string, prev_was_bad bool, f *os.File) error {
		if prev_was_bad {
			t.Fatal("Bad passphrase")
		}
		_, err := io.WriteString(f, "password\n")
		return err
	}))
	return ctx
}

func TestContext_Armor(t *testing.T) {
	ctx, err := New()
	checkError(t, err)

	ctx.SetArmor(true)
	if !ctx.Armor() {
		t.Error("expected armor set")
	}
	ctx.SetArmor(false)
	if ctx.Armor() {
		t.Error("expected armor not set")
	}
}

func TestContext_TextMode(t *testing.T) {
	ctx, err := New()
	checkError(t, err)

	ctx.SetTextMode(true)
	if !ctx.TextMode() {
		t.Error("expected textmode set")
	}
	ctx.SetTextMode(false)
	if ctx.TextMode() {
		t.Error("expected textmode not set")
	}
}

func TestContext_EngineInfo(t *testing.T) {
	ctx, err := New()
	checkError(t, err)

	testProto := ProtocolOpenPGP
	testFN := "testFN"
	testHomeDir := "testHomeDir"
	checkError(t, ctx.SetEngineInfo(testProto, testFN, testHomeDir))

	info := ctx.EngineInfo()
	compareEngineInfo(t, info, testProto, testFN, testHomeDir)

	// SetEngineInfo with empty strings works, using defaults which we don't know,
	// so just test that it doesn't fail.
	checkError(t, ctx.SetEngineInfo(testProto, testFN, ""))
	checkError(t, ctx.SetEngineInfo(testProto, "", testHomeDir))
}

func TestContext_Encrypt(t *testing.T) {
	ctx, err := New()
	checkError(t, err)

	keys, err := FindKeys("test@example.com", true)
	checkError(t, err)

	plain, err := NewDataBytes([]byte(testData))
	checkError(t, err)

	var buf bytes.Buffer
	cipher, err := NewDataWriter(&buf)
	checkError(t, err)

	checkError(t, ctx.Encrypt(keys, 0, plain, cipher))
	if buf.Len() < 1 {
		t.Error("Expected encrypted bytes, got empty buffer")
	}
}

func TestContext_Decrypt(t *testing.T) {
	ctx := ctxWithCallback(t)

	cipher, err := NewDataBytes([]byte(testCipherText))
	checkError(t, err)
	var buf bytes.Buffer
	plain, err := NewDataWriter(&buf)
	checkError(t, err)
	checkError(t, ctx.Decrypt(cipher, plain))
	diff(t, buf.Bytes(), []byte("Test message\n"))
}

func TestContext_DecryptVerify(t *testing.T) {
	ctx := ctxWithCallback(t)

	cipher, err := NewDataBytes([]byte(textSignedCipherText))
	checkError(t, err)
	var buf bytes.Buffer
	plain, err := NewDataWriter(&buf)
	checkError(t, err)
	checkError(t, ctx.DecryptVerify(cipher, plain))
	diff(t, buf.Bytes(), []byte("Test message\n"))
}

func TestContext_Sign(t *testing.T) {
	ctx := ctxWithCallback(t)

	key, err := ctx.GetKey("test@example.com", true)
	checkError(t, err)

	plain, err := NewDataBytes([]byte(testData))
	checkError(t, err)

	var buf bytes.Buffer
	signed, err := NewDataWriter(&buf)
	checkError(t, err)

	checkError(t, ctx.Sign([]*Key{key}, plain, signed, SigModeNormal))
	if buf.Len() < 1 {
		t.Error("Expected signed bytes, got empty buffer")
	}
}

func TestContext_Verify(t *testing.T) {
	ctx, err := New()
	checkError(t, err)

	_, err = ctx.GetKey("test@example.com", false)
	checkError(t, err)

	signed, err := NewDataBytes([]byte(testSignedText))
	checkError(t, err)

	var buf bytes.Buffer
	plain, err := NewDataWriter(&buf)
	checkError(t, err)

	_, sigs, err := ctx.Verify(signed, nil, plain)
	checkError(t, err)

	if len(sigs) != 1 {
		t.Error("Expected 1 signature")
	}
	sig := sigs[0]
	// Normalize
	expectedSig := Signature{
		Summary:        SigSumValid | SigSumGreen,
		Fingerprint:    "44B646DC347C31E867FF4F450327FFB0229F6136",
		Status:         nil,
		Timestamp:      sig.Timestamp,    // Ignore in comparison
		ExpTimestamp:   sig.ExpTimestamp, // Ignore in comparison
		WrongKeyUsage:  false,
		PKATrust:       0,
		ChainModel:     false,
		Validity:       ValidityFull,
		ValidityReason: nil,
		PubkeyAlgo:     sig.PubkeyAlgo, // Ignore in comparison
		HashAlgo:       sig.HashAlgo,   // Ignore in comparison
	}
	if sig != expectedSig {
		t.Errorf("Signature verification does not match: %#v vs. %#v", sig, expectedSig)
	}

	diff(t, buf.Bytes(), []byte("Test message\n"))
}

func TestContext_Import(t *testing.T) {
	homeDir, err := ioutil.TempDir("", "gpgme-import-test")
	checkError(t, err)
	defer os.RemoveAll(homeDir)

	ctx, err := New()
	checkError(t, err)
	checkError(t, ctx.SetEngineInfo(ProtocolOpenPGP, "", homeDir))

	f, err := os.Open("./testdata/pubkeys.gpg")
	checkError(t, err)
	defer f.Close()
	dh, err := NewDataFile(f)
	checkError(t, err)
	defer dh.Close()

	res, err := ctx.Import(dh)
	checkError(t, err)

	for _, v := range []struct {
		Name     string
		Value    int
		Expected int
	}{
		{"Considered", res.Considered, 1},
		{"NoUserID", res.NoUserID, 0},
		{"Imported", res.Imported, 1},
		// Do not test ImportedRSA, as of gnupg 2.1.11 the value is always 0.
		{"Unchanged", res.Unchanged, 0},
		{"NewUserIDs", res.NewUserIDs, 0},
		{"NewSubKeys", res.NewSubKeys, 0},
		{"NewSignatures", res.NewSignatures, 0},
		{"NewRevocations", res.NewRevocations, 0},
		{"SecretRead", res.SecretRead, 0},
		{"SecretImported", res.SecretImported, 0},
		{"SecretUnchanged", res.SecretUnchanged, 0},
		{"NotImported", res.NotImported, 0},
	} {
		if v.Value != v.Expected {
			t.Errorf("Unexpected import result field %s, value %d, expected %d", v.Name, v.Value, v.Expected)
		}
	}
	expectedStatus := ImportStatus{
		Fingerprint: "44B646DC347C31E867FF4F450327FFB0229F6136",
		Result:      nil,
		Status:      ImportNew,
	}
	if len(res.Imports) != 1 {
		t.Errorf("Unexpected number of import status values %d", len(res.Imports))
	} else if res.Imports[0] != expectedStatus {
		t.Errorf("Import status does not match: %#v vs. %#v", res.Imports[0], expectedStatus)
	}
}

func TestContext_Export(t *testing.T) {
	ctx, err := New()
	checkError(t, err)

	data, err := NewData()
	checkError(t, err)

	err = ctx.Export("", 0, data)
	checkError(t, err)

	_, _ = data.Seek(0, 0)
	allKeys, err := ioutil.ReadAll(data)
	checkError(t, err)
	if len(allKeys) < 1 {
		t.Error("Expected exported keys, got empty buffer")
	}
}

func TestContext_AssuanSend(t *testing.T) {
	// Launch a gpg-agent in daemon mode
	cmd := exec.Command("gpg-connect-agent", "--verbose", "/bye")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err := cmd.Run()
	if err != nil {
		t.Skip(err)
	}

	ctx, err := New()
	checkError(t, err)
	err = ctx.SetProtocol(ProtocolAssuan)
	checkError(t, err)

	// NOP should simply succeed
	err = ctx.AssuanSend("NOP", nil, nil, nil)
	checkError(t, err)

	// GETINFO should return a version in Data
	var versionData []byte
	err = ctx.AssuanSend("GETINFO version",
		func(data []byte) error {
			versionData = data
			return nil
		},
		nil,
		nil,
	)
	checkError(t, err)
	if len(versionData) == 0 {
		t.Error("Expected version data")
	}

	err = ctx.AssuanSend("KILLAGENT", nil, nil, nil)
	checkError(t, err)
}

func isVersion(t testing.TB, version string) bool {
	t.Helper()
	var info *EngineInfo
	info, err := GetEngineInfo()
	checkError(t, err)
	for info != nil {
		if info.Protocol() == ProtocolOpenPGP {
			if strings.Contains(info.FileName(), "gpg") && strings.HasPrefix(info.Version(), version) {
				return true
			}
			return false
		}
		info = info.Next()
	}
	return false
}

var gpgBins = []string{"gpg2", "gpg1", "gpg"}

// ensureVersion tries to setup gpgme with a specific version or skip
func ensureVersion(t testing.TB, version, msg string) {
	t.Helper()
	if isVersion(t, version) {
		return
	}
	for _, bin := range gpgBins {
		path, err := exec.LookPath(bin)
		if err != nil {
			continue
		}
		if err := SetEngineInfo(ProtocolOpenPGP, path, absTestGPGHome()); err != nil {
			continue
		}
		if isVersion(t, version) {
			return
		}
	}
	t.Skip(msg)
}

func diff(t testing.TB, dst, src []byte) {
	t.Helper()
	line := 1
	offs := 0 // line offset
	for i := 0; i < len(dst) && i < len(src); i++ {
		d := dst[i]
		s := src[i]
		if d != s {
			t.Errorf("dst:%d: %s\n", line, dst[offs:i+1])
			t.Errorf("src:%d: %s\n", line, src[offs:i+1])
			return
		}
		if s == '\n' {
			line++
			offs = i + 1
		}
	}
	if len(dst) != len(src) {
		t.Errorf("len(dst) = %d, len(src) = %d\nsrc = %q", len(dst), len(src), src)
	}
}

func checkError(t testing.TB, err error) {
	t.Helper()
	if err == nil {
		return
	}
	t.Fatal(err)
}

func absTestGPGHome() string {
	f, err := filepath.Abs(testGPGHome)
	if err != nil {
		panic(err)
	}
	return f
}
