package aliases

import (
	"io/ioutil"
	"os"
	"reflect"
	"testing"
)

type Cases []struct {
	addr   string
	expect []Recipient
}

func (cases Cases) check(t *testing.T, r *Resolver) {
	for _, c := range cases {
		got, err := r.Resolve(c.addr)
		if err != nil {
			t.Errorf("case %q, got error: %v", c.addr, err)
			continue
		}
		if !reflect.DeepEqual(got, c.expect) {
			t.Errorf("case %q, got %+v, expected %+v", c.addr, got, c.expect)
		}
	}
}

func mustExist(t *testing.T, r *Resolver, addrs ...string) {
	for _, addr := range addrs {
		if _, ok := r.Exists(addr); !ok {
			t.Errorf("address %q does not exist, it should", addr)
		}
	}
}

func mustNotExist(t *testing.T, r *Resolver, addrs ...string) {
	for _, addr := range addrs {
		if _, ok := r.Exists(addr); ok {
			t.Errorf("address %q exists, it should not", addr)
		}
	}
}

func TestBasic(t *testing.T) {
	resolver := NewResolver()
	resolver.aliases = map[string][]Recipient{
		"a@b": {{"c@d", EMAIL}, {"e@f", EMAIL}},
		"e@f": {{"cmd", PIPE}},
		"cmd": {{"x@y", EMAIL}}, // it's a trap!
	}

	cases := Cases{
		{"a@b", []Recipient{{"c@d", EMAIL}, {"cmd", PIPE}}},
		{"e@f", []Recipient{{"cmd", PIPE}}},
		{"x@y", []Recipient{{"x@y", EMAIL}}},
	}
	cases.check(t, resolver)

	mustExist(t, resolver, "a@b", "e@f", "cmd")
	mustNotExist(t, resolver, "x@y")
}

func TestAddrRewrite(t *testing.T) {
	resolver := NewResolver()
	resolver.AddDomain("def")
	resolver.AddDomain("p-q.com")
	resolver.aliases = map[string][]Recipient{
		"abc@def":  {{"x@y", EMAIL}},
		"ñoño@def": {{"x@y", EMAIL}},
		"recu@def": {{"ab+cd@p-q.com", EMAIL}},
	}
	resolver.DropChars = ".~"
	resolver.SuffixSep = "-+"

	cases := Cases{
		{"abc@def", []Recipient{{"x@y", EMAIL}}},
		{"a.b.c@def", []Recipient{{"x@y", EMAIL}}},
		{"a~b~c@def", []Recipient{{"x@y", EMAIL}}},
		{"a.b~c@def", []Recipient{{"x@y", EMAIL}}},
		{"abc-ñaca@def", []Recipient{{"x@y", EMAIL}}},
		{"abc-ñaca@def", []Recipient{{"x@y", EMAIL}}},
		{"abc-xyz@def", []Recipient{{"x@y", EMAIL}}},
		{"abc+xyz@def", []Recipient{{"x@y", EMAIL}}},
		{"abc-x.y+z@def", []Recipient{{"x@y", EMAIL}}},

		{"ñ.o~ño-ñaca@def", []Recipient{{"x@y", EMAIL}}},

		// Don't mess with the domain, even if it's known.
		{"a.bc-ñaca@p-q.com", []Recipient{{"abc@p-q.com", EMAIL}}},

		// Clean the right hand side too (if it's a local domain).
		{"recu+blah@def", []Recipient{{"ab@p-q.com", EMAIL}}},

		// We should not mess with emails for domains we don't know.
		{"xy@z.com", []Recipient{{"xy@z.com", EMAIL}}},
		{"x.y@z.com", []Recipient{{"x.y@z.com", EMAIL}}},
		{"x-@y-z.com", []Recipient{{"x-@y-z.com", EMAIL}}},
		{"x+blah@y", []Recipient{{"x+blah@y", EMAIL}}},
	}
	cases.check(t, resolver)
}

func TestExistsRewrite(t *testing.T) {
	resolver := NewResolver()
	resolver.AddDomain("def")
	resolver.AddDomain("p-q.com")
	resolver.aliases = map[string][]Recipient{
		"abc@def":  {{"x@y", EMAIL}},
		"ñoño@def": {{"x@y", EMAIL}},
		"recu@def": {{"ab+cd@p-q.com", EMAIL}},
	}
	resolver.DropChars = ".~"
	resolver.SuffixSep = "-+"

	mustExist(t, resolver, "abc@def", "a.bc+blah@def", "ño.ño@def")
	mustNotExist(t, resolver, "abc@d.ef", "nothere@def")

	cases := []struct {
		addr         string
		expectAddr   string
		expectExists bool
	}{
		{"abc@def", "abc@def", true},
		{"abc+blah@def", "abc@def", true},
		{"a.b~c@def", "abc@def", true},
		{"a.bc+blah@def", "abc@def", true},

		{"a.bc@unknown", "a.bc@unknown", false},
		{"x.yz@def", "xyz@def", false},
		{"x.yz@d.ef", "x.yz@d.ef", false},
	}
	for _, c := range cases {
		addr, exists := resolver.Exists(c.addr)
		if addr != c.expectAddr {
			t.Errorf("%q: expected addr %q, got %q",
				c.addr, c.expectAddr, addr)
		}
		if exists != c.expectExists {
			t.Errorf("%q: expected exists %v, got %v",
				c.addr, c.expectExists, exists)
		}
	}
}

func TestTooMuchRecursion(t *testing.T) {
	resolver := Resolver{}
	resolver.aliases = map[string][]Recipient{
		"a@b": {{"c@d", EMAIL}},
		"c@d": {{"a@b", EMAIL}},
	}

	rs, err := resolver.Resolve("a@b")
	if err != ErrRecursionLimitExceeded {
		t.Errorf("expected ErrRecursionLimitExceeded, got %v", err)
	}

	if rs != nil {
		t.Errorf("expected nil recipients, got %+v", rs)
	}
}

func mustWriteFile(t *testing.T, content string) string {
	f, err := ioutil.TempFile("", "aliases_test")
	if err != nil {
		t.Fatalf("failed to get temp file: %v", err)
	}
	defer f.Close()

	_, err = f.WriteString(content)
	if err != nil {
		t.Fatalf("failed to write temp file: %v", err)
	}

	return f.Name()
}

func TestAddFile(t *testing.T) {
	cases := []struct {
		contents string
		expected []Recipient
	}{
		{"\n", []Recipient{{"a@dom", EMAIL}}},
		{" # Comment\n", []Recipient{{"a@dom", EMAIL}}},
		{":\n", []Recipient{{"a@dom", EMAIL}}},
		{"a: \n", []Recipient{{"a@dom", EMAIL}}},
		{"a@dom: b@c \n", []Recipient{{"a@dom", EMAIL}}},

		{"a: b\n", []Recipient{{"b@dom", EMAIL}}},
		{"a:b\n", []Recipient{{"b@dom", EMAIL}}},
		{"a : b \n", []Recipient{{"b@dom", EMAIL}}},
		{"a : b, \n", []Recipient{{"b@dom", EMAIL}}},

		{"a: |cmd\n", []Recipient{{"cmd", PIPE}}},
		{"a:|cmd\n", []Recipient{{"cmd", PIPE}}},
		{"a:| cmd \n", []Recipient{{"cmd", PIPE}}},
		{"a  :| cmd \n", []Recipient{{"cmd", PIPE}}},
		{"a: | cmd  arg1 arg2\n", []Recipient{{"cmd  arg1 arg2", PIPE}}},

		{"a: c@d, e@f, g\n",
			[]Recipient{{"c@d", EMAIL}, {"e@f", EMAIL}, {"g@dom", EMAIL}}},

		// Invalid pipe aliases, should be ignored.
		{"a:|\n", []Recipient{{"a@dom", EMAIL}}},
		{"a:| \n", []Recipient{{"a@dom", EMAIL}}},
	}

	for _, c := range cases {
		fname := mustWriteFile(t, c.contents)
		defer os.Remove(fname)

		resolver := NewResolver()
		err := resolver.AddAliasesFile("dom", fname)
		if err != nil {
			t.Fatalf("error adding file: %v", err)
		}

		got, err := resolver.Resolve("a@dom")
		if err != nil {
			t.Errorf("case %q, got error: %v", c.contents, err)
			continue
		}
		if !reflect.DeepEqual(got, c.expected) {
			t.Errorf("case %q, got %v, expected %v", c.contents, got, c.expected)
		}
	}
}

const richFileContents = `
# This is a "complex" alias file, with a few tricky situations.
# It is used in TestRichFile.

# First some valid cases.
a: b
c: d@e, f,
x: | command

# The following is invalid, should be ignored.
a@dom: x@dom

# Overrides.
o1: a
o1: b

# Check that we normalize the right hand side.
aA: bB@dom-B

# Finally one to make the file NOT end in \n:
y: z`

func TestRichFile(t *testing.T) {
	fname := mustWriteFile(t, richFileContents)
	defer os.Remove(fname)

	resolver := NewResolver()
	err := resolver.AddAliasesFile("dom", fname)
	if err != nil {
		t.Fatalf("failed to add file: %v", err)
	}

	cases := Cases{
		{"a@dom", []Recipient{{"b@dom", EMAIL}}},
		{"c@dom", []Recipient{{"d@e", EMAIL}, {"f@dom", EMAIL}}},
		{"x@dom", []Recipient{{"command", PIPE}}},
		{"o1@dom", []Recipient{{"b@dom", EMAIL}}},
		{"aA@dom", []Recipient{{"bb@dom-b", EMAIL}}},
		{"aa@dom", []Recipient{{"bb@dom-b", EMAIL}}},
		{"y@dom", []Recipient{{"z@dom", EMAIL}}},
	}
	cases.check(t, resolver)
}

func TestManyFiles(t *testing.T) {
	files := map[string]string{
		"d1":      mustWriteFile(t, "a: b\nc:d@e"),
		"domain2": mustWriteFile(t, "a: b\nc:d@e"),
		"dom3":    mustWriteFile(t, "x: y, z"),
		"dom4":    mustWriteFile(t, "a: |cmd"),

		// Cross-domain.
		"xd1": mustWriteFile(t, "a: b@xd2"),
		"xd2": mustWriteFile(t, "b: |cmd"),
	}
	for _, fname := range files {
		defer os.Remove(fname)
	}

	resolver := NewResolver()
	for domain, fname := range files {
		err := resolver.AddAliasesFile(domain, fname)
		if err != nil {
			t.Fatalf("failed to add file: %v", err)
		}
	}

	check := func() {
		cases := Cases{
			{"a@d1", []Recipient{{"b@d1", EMAIL}}},
			{"c@d1", []Recipient{{"d@e", EMAIL}}},
			{"x@d1", []Recipient{{"x@d1", EMAIL}}},
			{"a@domain2", []Recipient{{"b@domain2", EMAIL}}},
			{"c@domain2", []Recipient{{"d@e", EMAIL}}},
			{"x@dom3", []Recipient{{"y@dom3", EMAIL}, {"z@dom3", EMAIL}}},
			{"a@dom4", []Recipient{{"cmd", PIPE}}},
			{"a@xd1", []Recipient{{"cmd", PIPE}}},
		}
		cases.check(t, resolver)
	}

	check()

	// Reload, and check again just in case.
	if err := resolver.Reload(); err != nil {
		t.Fatalf("failed to reload: %v", err)
	}

	check()
}
