package main

import (
	"bufio"
	"bytes"
	"compress/gzip"
	"fmt"
	"io/ioutil"
	"os"
	"testing"
	"time"

	"github.com/letsencrypt/boulder/test"
)

func TestOpenFile(t *testing.T) {
	tmpPlain, err := ioutil.TempFile(os.TempDir(), "plain")
	test.AssertNotError(t, err, "failed to create temporary file")
	defer os.Remove(tmpPlain.Name())
	_, err = tmpPlain.Write([]byte("test-1\ntest-2"))
	test.AssertNotError(t, err, "failed to write to temp file")
	tmpPlain.Close()

	tmpGzip, err := ioutil.TempFile(os.TempDir(), "gzip-*.gz")
	test.AssertNotError(t, err, "failed to create temporary file")
	defer os.Remove(tmpGzip.Name())
	gzipWriter := gzip.NewWriter(tmpGzip)
	_, err = gzipWriter.Write([]byte("test-1\ntest-2"))
	test.AssertNotError(t, err, "failed to write to temp file")
	gzipWriter.Flush()
	gzipWriter.Close()
	tmpGzip.Close()

	checkFile := func(path string) {
		t.Helper()
		scanner, err := openFile(path)
		test.AssertNotError(t, err, fmt.Sprintf("failed to open %q", path))
		var lines []string
		for scanner.Scan() {
			lines = append(lines, scanner.Text())
		}
		test.AssertNotError(t, scanner.Err(), fmt.Sprintf("failed to read from %q", path))
		test.AssertEquals(t, len(lines), 2)
		test.AssertDeepEquals(t, lines, []string{"test-1", "test-2"})
	}

	checkFile(tmpPlain.Name())
	checkFile(tmpGzip.Name())
}

func TestLoadMap(t *testing.T) {
	testTime := time.Time{}.Add(time.Hour).Add(time.Nanosecond * 123456000)
	testTime = testTime.In(time.FixedZone("UTC-8", -8*60*60))

	tmpA, err := ioutil.TempFile(os.TempDir(), "va-a")
	test.AssertNotError(t, err, "failed to create temporary file")
	defer os.Remove(tmpA.Name())
	formattedTime := testTime.Format(time.RFC3339Nano)
	_, err = tmpA.Write([]byte(fmt.Sprintf(`random
%s Checked CAA records for example.com, [Present: true, asd
random
%s Checked CAA records for beep.boop.com, [Present: false, asd`, formattedTime, formattedTime)))
	test.AssertNotError(t, err, "failed to write to temp file")
	tmpA.Close()
	tmpB, err := ioutil.TempFile(os.TempDir(), "va-b")
	test.AssertNotError(t, err, "failed to create temporary file")
	defer os.Remove(tmpB.Name())
	formattedTime = testTime.Add(time.Hour).Format(time.RFC3339Nano)
	_, err = tmpB.Write([]byte(fmt.Sprintf(`random
%s Checked CAA records for example.com, [Present: true, asd
random
%s Checked CAA records for beep.boop.com, [Present: false, asd`, formattedTime, formattedTime)))
	test.AssertNotError(t, err, "failed to write to temp file")
	tmpB.Close()

	m, err := loadMap([]string{tmpA.Name(), tmpB.Name()})
	test.AssertNotError(t, err, "fail to load log files")
	test.AssertEquals(t, len(m), 3)
	test.Assert(t, m["example.com"][0].Equal(testTime), "wrong time")
	test.Assert(t, m["example.com"][1].Equal(testTime.Add(time.Hour)), "wrong time")
	test.Assert(t, m["beep.boop.com"][0].Equal(testTime), "wrong time")
	test.Assert(t, m["beep.boop.com"][1].Equal(testTime.Add(time.Hour)), "wrong time")
	test.Assert(t, m["boop.com"][0].Equal(testTime), "wrong time")
	test.Assert(t, m["boop.com"][1].Equal(testTime.Add(time.Hour)), "wrong time")
}

func TestCheckIssuances(t *testing.T) {
	testTime := time.Time{}.Add(time.Hour).Add(time.Nanosecond * 123456000)
	testTime = testTime.In(time.FixedZone("UTC-8", -8*60*60))

	checkedMap := map[string][]time.Time{
		"example.com": {
			testTime.Add(time.Hour),
			testTime.Add(3 * time.Hour),
		},
		"2.example.com": {
			testTime.Add(time.Hour),
		},
		"4.example.com": {
			testTime.Add(time.Hour),
		},
		"5.example.com": {
			testTime.Add(time.Hour),
		},
		"6.example.com": {
			testTime.Add(time.Hour + time.Minute),
		},
	}

	raString := fmt.Sprintf(`random
%s Certificate request - successful JSON={"SerialNumber": "1", "Names":["example.com"], "Requester":0}
random
%s Certificate request - successful JSON={"SerialNumber": "2", "Names":["2.example.com", "3.example.com"], "Requester":0}
%s Certificate request - successful JSON={"SerialNumber": "3", "Names":["4.example.com"], "Requester":0}
%s Certificate request - successful JSON={"SerialNumber": "4", "Names":["5.example.com"], "Requester":0}
%s Certificate request - successful JSON={"SerialNumber": "5", "Names":["6.example.com"], "Requester":0}
%s Certificate request - successful JSON={"SerialNumber": "7", "Names":["7.example.com"], "Requester":0}
%s Certificate request - successful JSON={"SerialNumber": "8", "Names":["8.example.com"], "Requester":0}
%s Certificate request - successful JSON={"SerialNumber": "9", "Names":["9.example.com"], "Requester":0}
%s Certificate request - successful JSON={"SerialNumber": "10", "Names":["10.example.com"], "Requester":0}
random`,
		// example.com: CAA @ +1:00, Issue @ +2:00, CAA @ +3:00. (PASS, one valid CAA check, one invalid)
		testTime.Add(time.Hour*2).Format(time.RFC3339Nano),
		// 2.example.com: CAA @ +1:00, Issue @ +2:00. (PASS)
		// 3.example.com: Issue @ +2:00. (FAIL, no CAA check)
		testTime.Add(time.Hour*2).Format(time.RFC3339Nano),
		// 4.example.com: Issue @ +0:30, CAA @ +1:00. (FAIL, but has CAA check within 1h)
		testTime.Add(30*time.Minute).Format(time.RFC3339Nano),
		// 5.example.com: CAA @ +1:00, Issue @ +10:00. (FAIL, CAA check expired >8h)
		testTime.Add(10*time.Hour).Format(time.RFC3339Nano),
		// 6.example.com: Issue @ +1:00, CAA @ +1:01. (PASS, has CAA check within tolerance)
		testTime.Add(time.Hour).Format(time.RFC3339Nano),
		// 7.example.com: Issue @ +12:00 (PASS, no CAA check but issued after latest)
		testTime.Add(12*time.Hour).Format(time.RFC3339Nano),
		// 8.example.com: Issue @ +11:00 (FAIL, no CAA check and on latest boundary)
		testTime.Add(11*time.Hour).Format(time.RFC3339Nano),
		// 9.example.com: Issue @ -2:00 (PASS, no CAA check but issued before earliest)
		testTime.Add(-2*time.Hour).Format(time.RFC3339Nano),
		// 10.example.com: Issue @ -1:00 (FAIL, no CAA check and issued at earliest boundary)
		testTime.Add(-1*time.Hour).Format(time.RFC3339Nano),
	)

	for _, testCase := range []struct {
		name           string
		expectedErrors string
		earliest       time.Time
		latest         time.Time
	}{
		{
			"with-timespan",
			"Issuance missing CAA checks: issued at=0000-12-31 19:00:00.123456 -0800 -0800, serial=2, requester=0, names=[2.example.com 3.example.com], missing checks for names=[3.example.com], timeError=[+Inf]\n" +
				"Issuance missing CAA checks: issued at=0000-12-31 17:30:00.123456 -0800 -0800, serial=3, requester=0, names=[4.example.com], missing checks for names=[4.example.com], timeError=[1800.000]\n" +
				"Issuance missing CAA checks: issued at=0001-01-01 03:00:00.123456 -0800 -0800, serial=4, requester=0, names=[5.example.com], missing checks for names=[5.example.com], timeError=[+Inf]\n" +
				"Issuance missing CAA checks: issued at=0001-01-01 04:00:00.123456 -0800 -0800, serial=8, requester=0, names=[8.example.com], missing checks for names=[8.example.com], timeError=[+Inf]\n" +
				"Issuance missing CAA checks: issued at=0000-12-31 16:00:00.123456 -0800 -0800, serial=10, requester=0, names=[10.example.com], missing checks for names=[10.example.com], timeError=[+Inf]\n",
			testTime.Add(-1 * time.Hour),
			testTime.Add(11 * time.Hour),
		},
		{
			"no-timespan",
			"Issuance missing CAA checks: issued at=0000-12-31 19:00:00.123456 -0800 -0800, serial=2, requester=0, names=[2.example.com 3.example.com], missing checks for names=[3.example.com], timeError=[+Inf]\n" +
				"Issuance missing CAA checks: issued at=0000-12-31 17:30:00.123456 -0800 -0800, serial=3, requester=0, names=[4.example.com], missing checks for names=[4.example.com], timeError=[1800.000]\n" +
				"Issuance missing CAA checks: issued at=0001-01-01 03:00:00.123456 -0800 -0800, serial=4, requester=0, names=[5.example.com], missing checks for names=[5.example.com], timeError=[+Inf]\n" +
				"Issuance missing CAA checks: issued at=0001-01-01 05:00:00.123456 -0800 -0800, serial=7, requester=0, names=[7.example.com], missing checks for names=[7.example.com], timeError=[+Inf]\n" +
				"Issuance missing CAA checks: issued at=0001-01-01 04:00:00.123456 -0800 -0800, serial=8, requester=0, names=[8.example.com], missing checks for names=[8.example.com], timeError=[+Inf]\n" +
				"Issuance missing CAA checks: issued at=0000-12-31 15:00:00.123456 -0800 -0800, serial=9, requester=0, names=[9.example.com], missing checks for names=[9.example.com], timeError=[+Inf]\n" +
				"Issuance missing CAA checks: issued at=0000-12-31 16:00:00.123456 -0800 -0800, serial=10, requester=0, names=[10.example.com], missing checks for names=[10.example.com], timeError=[+Inf]\n",
			time.Time{},
			time.Time{},
		},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			raScanner := bufio.NewScanner(bytes.NewBuffer([]byte(raString)))
			stderr, err := ioutil.TempFile(os.TempDir(), "stderr")
			test.AssertNotError(t, err, "failed creating temporary file")
			defer os.Remove(stderr.Name())

			timeTolerance := 10 * time.Minute
			foundErrors, err := checkIssuances(raScanner, checkedMap, timeTolerance, testCase.earliest, testCase.latest, stderr)
			test.AssertNotError(t, err, "checkIssuances failed")

			test.Assert(t, foundErrors, "checkIssuances should indicate it found errors")
			stderrCont, err := ioutil.ReadFile(stderr.Name())
			test.AssertNotError(t, err, "failed to read temporary file")
			test.AssertEquals(t, string(stderrCont), testCase.expectedErrors)
		})
	}
}
