package ocsp

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"testing"
	"time"

	"github.com/cloudflare/cfssl/api"
	"github.com/cloudflare/cfssl/helpers"
	"github.com/cloudflare/cfssl/ocsp"

	goocsp "golang.org/x/crypto/ocsp"
)

const (
	testCaFile       = "../../ocsp/testdata/ca.pem"
	testRespCertFile = "../../ocsp/testdata/server.crt"
	testKeyFile      = "../../ocsp/testdata/server.key"
	testCertFile     = "../../ocsp/testdata/cert.pem"
)

func newTestHandler(t *testing.T) http.Handler {
	// arbitrary duration
	dur, _ := time.ParseDuration("1ms")
	s, err := ocsp.NewSignerFromFile(testCaFile, testRespCertFile, testKeyFile, dur)
	if err != nil {
		t.Fatalf("Signer creation failed %v", err)
	}
	return NewHandler(s)
}

func TestNewHandler(t *testing.T) {
	newTestHandler(t)
}

func newSignServer(t *testing.T) *httptest.Server {
	ts := httptest.NewServer(newTestHandler(t))
	return ts
}

func testSignFile(t *testing.T, certFile, status string, reason int, revokedAt string, hash string) (resp *http.Response, body []byte) {
	ts := newSignServer(t)
	defer ts.Close()

	obj := map[string]interface{}{}
	if certFile != "" {
		c, err := os.ReadFile(certFile)
		if err != nil {
			t.Fatal(err)
		}
		obj["certificate"] = string(c)
	}
	if status != "" {
		obj["status"] = status
	}
	obj["reason"] = reason
	if revokedAt != "" {
		obj["revoked_at"] = revokedAt
	}
	obj["issuer_hash"] = hash

	blob, err := json.Marshal(obj)
	if err != nil {
		t.Fatal(err)
	}

	resp, err = http.Post(ts.URL, "application/json", bytes.NewReader(blob))
	if err != nil {
		t.Fatal(err)
	}
	body, err = io.ReadAll(resp.Body)
	if err != nil {
		t.Fatal(err)
	}
	return
}

type signTest struct {
	CertificateFile    string
	Status             string
	Reason             int
	RevokedAt          string
	ExpectedHTTPStatus int
	ExpectedSuccess    bool
	ExpectedErrorCode  int
	IssuerHash         string
}

var signTests = []signTest{
	{
		CertificateFile:    testCertFile,
		ExpectedHTTPStatus: http.StatusOK,
		ExpectedSuccess:    true,
		ExpectedErrorCode:  0,
	},
	{
		CertificateFile:    testCertFile,
		Status:             "revoked",
		Reason:             1,
		ExpectedHTTPStatus: http.StatusOK,
		ExpectedSuccess:    true,
		ExpectedErrorCode:  0,
	},
	{
		CertificateFile:    testCertFile,
		Status:             "revoked",
		RevokedAt:          "now",
		ExpectedHTTPStatus: http.StatusOK,
		ExpectedSuccess:    true,
		ExpectedErrorCode:  0,
	},
	{
		CertificateFile:    testCertFile,
		Status:             "revoked",
		RevokedAt:          "2015-08-15",
		ExpectedHTTPStatus: http.StatusOK,
		ExpectedSuccess:    true,
		ExpectedErrorCode:  0,
	},
	{
		CertificateFile:    testCertFile,
		Status:             "revoked",
		RevokedAt:          "a",
		ExpectedHTTPStatus: http.StatusBadRequest,
		ExpectedSuccess:    false,
		ExpectedErrorCode:  http.StatusBadRequest,
	},
	{
		CertificateFile:    "",
		Status:             "",
		ExpectedHTTPStatus: http.StatusBadRequest,
		ExpectedSuccess:    false,
		ExpectedErrorCode:  http.StatusBadRequest,
	},
	{
		CertificateFile:    testCertFile,
		Status:             "_",
		ExpectedHTTPStatus: http.StatusBadRequest,
		ExpectedSuccess:    false,
		ExpectedErrorCode:  8200,
	},
	{
		CertificateFile:    testCertFile,
		IssuerHash:         "SHA256",
		ExpectedHTTPStatus: http.StatusOK,
		ExpectedSuccess:    true,
		ExpectedErrorCode:  0,
	},
	{
		CertificateFile:    testCertFile,
		IssuerHash:         "MD4",
		ExpectedHTTPStatus: http.StatusBadRequest,
		ExpectedSuccess:    false,
		ExpectedErrorCode:  http.StatusBadRequest,
	},
}

func TestSign(t *testing.T) {
	for i, test := range signTests {
		resp, body := testSignFile(t, test.CertificateFile, test.Status, test.Reason, test.RevokedAt, test.IssuerHash)
		if resp.StatusCode != test.ExpectedHTTPStatus {
			t.Logf("Test %d: expected: %d, have %d", i, test.ExpectedHTTPStatus, resp.StatusCode)
			t.Fatal(resp.Status, test.ExpectedHTTPStatus, string(body))
		}

		message := new(api.Response)
		err := json.Unmarshal(body, message)
		if err != nil {
			t.Logf("failed to read response body: %v", err)
			t.Fatal(resp.Status, test.ExpectedHTTPStatus, message)
		}

		if test.ExpectedSuccess != message.Success {
			t.Logf("Test %d: expected: %v, have %v", i, test.ExpectedSuccess, message.Success)
			t.Fatal(resp.Status, test.ExpectedHTTPStatus, message)
		}
		if !test.ExpectedSuccess {
			if test.ExpectedErrorCode != message.Errors[0].Code {
				t.Fatalf("Test %d: expected: %v, have %v", i, test.ExpectedErrorCode, message.Errors[0].Code)
				t.Fatal(resp.Status, test.ExpectedHTTPStatus, message)
			}
			continue
		}

		result, ok := message.Result.(map[string]interface{})
		if !ok {
			t.Logf("failed to read result")
			t.Fatal(resp.Status, test.ExpectedHTTPStatus, string(body))
		}
		b64Resp, ok := result["ocspResponse"].(string)
		if !ok {
			t.Logf("failed to find ocspResponse")
			t.Fatal(resp.Status, test.ExpectedHTTPStatus, string(body))
		}

		der, err := base64.StdEncoding.DecodeString(b64Resp)
		if err != nil {
			t.Logf("failed to decode base64")
			t.Fatal(resp.Status, test.ExpectedHTTPStatus, b64Resp)
		}

		ocspResp, err := goocsp.ParseResponse(der, nil)
		if err != nil {
			t.Logf("failed to parse ocsp response: %v", err)
			t.Fatal(resp.Status, test.ExpectedHTTPStatus, b64Resp)
		}

		// should default to good
		if test.Status == "" {
			test.Status = "good"
		}
		intStatus := ocsp.StatusCode[test.Status]
		if ocspResp.Status != intStatus {
			t.Fatalf("Test %d incorrect status: expected: %v, have %v", i, intStatus, ocspResp.Status)
			t.Fatal(ocspResp.Status, intStatus, ocspResp)
		}

		if test.Status == "revoked" {
			if ocspResp.RevocationReason != test.Reason {
				t.Fatalf("Test %d incorrect reason: expected: %v, have %v", i, test.Reason, ocspResp.RevocationReason)
				t.Fatal(ocspResp.RevocationReason, test.Reason, ocspResp)
			}

			var r time.Time
			if test.RevokedAt == "" || test.RevokedAt == "now" {
				r = time.Now().UTC().Truncate(helpers.OneDay)
			} else {
				r, _ = time.Parse("2006-01-02", test.RevokedAt)
			}

			if !ocspResp.RevokedAt.Truncate(helpers.OneDay).Equal(r) {
				t.Fatalf("Test %d incorrect revokedAt: expected: %v, have %v", i, r, ocspResp.RevokedAt)
				t.Fatal(ocspResp.RevokedAt, test.RevokedAt, ocspResp)
			}
		}
	}
}
