package airbrake

import (
	"bytes"
	"encoding/json"
	"io/ioutil"
	"net/http"
	"testing"
	"time"

	"github.com/Sirupsen/logrus"
	"gopkg.in/airbrake/gobrake.v2"
)

type customErr struct {
	msg string
}

func (e *customErr) Error() string {
	return e.msg
}

const (
	testAPIKey    = "abcxyz"
	testEnv       = "development"
	expectedClass = "*airbrake.customErr"
	expectedMsg   = "foo"
	unintendedMsg = "Airbrake will not see this string"
)

var (
	noticeChan = make(chan *gobrake.Notice, 1)
)

// TestLogEntryMessageReceived checks if invoking Logrus' log.Error
// method causes an XML payload containing the log entry message is received
// by a HTTP server emulating an Airbrake-compatible endpoint.
func TestLogEntryMessageReceived(t *testing.T) {
	log := logrus.New()
	hook := newTestHook()
	log.Hooks.Add(hook)

	log.Error(expectedMsg)

	select {
	case received := <-noticeChan:
		receivedErr := received.Errors[0]
		if receivedErr.Message != expectedMsg {
			t.Errorf("Unexpected message received: %s", receivedErr.Message)
		}
	case <-time.After(time.Second):
		t.Error("Timed out; no notice received by Airbrake API")
	}
}

// TestLogEntryMessageReceived confirms that, when passing an error type using
// logrus.Fields, a HTTP server emulating an Airbrake endpoint receives the
// error message returned by the Error() method on the error interface
// rather than the logrus.Entry.Message string.
func TestLogEntryWithErrorReceived(t *testing.T) {
	log := logrus.New()
	hook := newTestHook()
	log.Hooks.Add(hook)

	log.WithFields(logrus.Fields{
		"error": &customErr{expectedMsg},
	}).Error(unintendedMsg)

	select {
	case received := <-noticeChan:
		receivedErr := received.Errors[0]
		if receivedErr.Message != expectedMsg {
			t.Errorf("Unexpected message received: %s", receivedErr.Message)
		}
		if receivedErr.Type != expectedClass {
			t.Errorf("Unexpected error class: %s", receivedErr.Type)
		}
	case <-time.After(time.Second):
		t.Error("Timed out; no notice received by Airbrake API")
	}
}

// TestLogEntryWithNonErrorTypeNotReceived confirms that, when passing a
// non-error type using logrus.Fields, a HTTP server emulating an Airbrake
// endpoint receives the logrus.Entry.Message string.
//
// Only error types are supported when setting the 'error' field using
// logrus.WithFields().
func TestLogEntryWithNonErrorTypeNotReceived(t *testing.T) {
	log := logrus.New()
	hook := newTestHook()
	log.Hooks.Add(hook)

	log.WithFields(logrus.Fields{
		"error": expectedMsg,
	}).Error(unintendedMsg)

	select {
	case received := <-noticeChan:
		receivedErr := received.Errors[0]
		if receivedErr.Message != unintendedMsg {
			t.Errorf("Unexpected message received: %s", receivedErr.Message)
		}
	case <-time.After(time.Second):
		t.Error("Timed out; no notice received by Airbrake API")
	}
}

func TestLogEntryWithCustomFields(t *testing.T) {
	log := logrus.New()
	hook := newTestHook()
	log.Hooks.Add(hook)

	log.WithFields(logrus.Fields{
		"user_id": "123",
	}).Error(unintendedMsg)

	select {
	case received := <-noticeChan:
		receivedErr := received.Errors[0]
		if receivedErr.Message != unintendedMsg {
			t.Errorf("Unexpected message received: %s", receivedErr.Message)
		}
		if received.Context["user_id"] != "123" {
			t.Errorf("Expected message to contain Context[\"user_id\"] == \"123\" got %q", received.Context["user_id"])
		}
	case <-time.After(time.Second):
		t.Error("Timed out; no notice received by Airbrake API")
	}
}

func TestLogEntryWithHTTPRequestFields(t *testing.T) {
	log := logrus.New()
	hook := newTestHook()
	log.Hooks.Add(hook)

	req, err := http.NewRequest("GET", "http://example.com", nil)
	if err != nil {
		t.Fatal(err)
	}

	log.WithFields(logrus.Fields{
		"user_id": "123",
		"request": req,
	}).Error(unintendedMsg)

	select {
	case received := <-noticeChan:
		receivedErr := received.Errors[0]
		if receivedErr.Message != unintendedMsg {
			t.Errorf("Unexpected message received: %s", receivedErr.Message)
		}
		if received.Context["user_id"] != "123" {
			t.Errorf("Expected message to contain Context[\"user_id\"] == \"123\" got %q", received.Context["user_id"])
		}
		if received.Context["url"] != "http://example.com" {
			t.Errorf("Expected message to contain Context[\"url\"] == \"http://example.com\" got %q", received.Context["url"])
		}
	case <-time.After(time.Second):
		t.Error("Timed out; no notice received by Airbrake API")
	}
}

// Returns a new airbrakeHook with the test server proxied
func newTestHook() *airbrakeHook {
	// Make a http.Client with the transport
	httpClient := &http.Client{Transport: &FakeRoundTripper{}}

	hook := NewHook(123, testAPIKey, "production")
	hook.Airbrake.Client = httpClient
	return hook
}

// gobrake API doesn't allow to override endpoint, we need a http.Roundtripper
type FakeRoundTripper struct {
}

func (rt *FakeRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
	b, err := ioutil.ReadAll(r.Body)
	if err != nil {
		panic(err)
	}

	notice := &gobrake.Notice{}
	err = json.Unmarshal(b, notice)
	if err != nil {
		panic(err)
	}

	noticeChan <- notice

	jsonResponse := struct {
		Id string `json:"id"`
	}{"1"}

	sendResponse, _ := json.Marshal(jsonResponse)
	res := &http.Response{
		StatusCode: http.StatusCreated,
		Body:       ioutil.NopCloser(bytes.NewReader(sendResponse)),
		Header:     make(http.Header),
	}
	return res, nil
}
