package httpjson

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/influxdata/telegraf/testutil"
)

const validJSON = `
	{
		"parent": {
			"child": 3.0,
			"ignored_child": "hi"
		},
		"ignored_null": null,
		"integer": 4,
		"list": [3, 4],
		"ignored_parent": {
			"another_ignored_null": null,
			"ignored_string": "hello, world!"
		},
		"another_list": [4]
	}`

const validJSON2 = `{
  "user":{
    "hash_rate":0,
    "expected_24h_rewards":0,
    "total_rewards":0.000595109232,
    "paid_rewards":0,
    "unpaid_rewards":0.000595109232,
    "past_24h_rewards":0,
    "total_work":"5172625408",
    "blocks_found":0
  },
  "workers":{
    "brminer.1":{
      "hash_rate":0,
      "hash_rate_24h":0,
      "valid_shares":"6176",
      "stale_shares":"0",
      "invalid_shares":"0",
      "rewards":4.5506464e-5,
      "rewards_24h":0,
      "reset_time":1455409950
    },
    "brminer.2":{
      "hash_rate":0,
      "hash_rate_24h":0,
      "valid_shares":"0",
      "stale_shares":"0",
      "invalid_shares":"0",
      "rewards":0,
      "rewards_24h":0,
      "reset_time":1455936726
    },
    "brminer.3":{
      "hash_rate":0,
      "hash_rate_24h":0,
      "valid_shares":"0",
      "stale_shares":"0",
      "invalid_shares":"0",
      "rewards":0,
      "rewards_24h":0,
      "reset_time":1455936733
    }
  },
  "pool":{
    "hash_rate":114100000,
    "active_users":843,
    "total_work":"5015346808842682368",
    "pps_ratio":1.04,
    "pps_rate":7.655e-9
  },
  "network":{
    "hash_rate":1426117703,
    "block_number":944895,
    "time_per_block":156,
    "difficulty":51825.72835216,
    "next_difficulty":51916.15249019,
    "retarget_time":95053
  },
  "market":{
    "ltc_btc":0.00798,
    "ltc_usd":3.37801,
    "ltc_eur":3.113,
    "ltc_gbp":2.32807,
    "ltc_rub":241.796,
    "ltc_cny":21.3883,
    "btc_usd":422.852
  }
}`

const validJSONTags = `
	{
		"value": 15,
		"role": "master",
		"build": "123"
	}`

var expectedFields = map[string]interface{}{
	"parent_child":   float64(3),
	"list_0":         float64(3),
	"list_1":         float64(4),
	"another_list_0": float64(4),
	"integer":        float64(4),
}

const invalidJSON = "I don't think this is JSON"

const empty = ""

type mockHTTPClient struct {
	responseBody string
	statusCode   int
}

// Mock implementation of MakeRequest. Usually returns an http.Response with
// hard-coded responseBody and statusCode. However, if the request uses a
// nonstandard method, it uses status code 405 (method not allowed)
func (c *mockHTTPClient) MakeRequest(req *http.Request) (*http.Response, error) {
	resp := http.Response{}
	resp.StatusCode = c.statusCode

	// basic error checking on request method
	allowedMethods := []string{"GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT"}
	methodValid := false
	for _, method := range allowedMethods {
		if req.Method == method {
			methodValid = true
			break
		}
	}

	if !methodValid {
		resp.StatusCode = 405 // Method not allowed
	}

	resp.Body = io.NopCloser(strings.NewReader(c.responseBody))
	return &resp, nil
}

func (c *mockHTTPClient) SetHTTPClient(_ *http.Client) {
}

func (c *mockHTTPClient) HTTPClient() *http.Client {
	return nil
}

// Generates a pointer to an HTTPJSON object that uses a mock HTTP client.
// Parameters:
//
//	response  : Body of the response that the mock HTTP client should return
//	statusCode: HTTP status code the mock HTTP client should return
//
// Returns:
//
//	*HTTPJSON: Pointer to an HTTPJSON object that uses the generated mock HTTP client
func genMockHTTPJSON(response string, statusCode int) []*HTTPJSON {
	return []*HTTPJSON{
		{
			client: &mockHTTPClient{responseBody: response, statusCode: statusCode},
			Servers: []string{
				"http://server1.example.com/metrics/",
				"http://server2.example.com/metrics/",
			},
			Name:   "my_webapp",
			Method: "GET",
			Parameters: map[string]string{
				"httpParam1": "12",
				"httpParam2": "the second parameter",
			},
			Headers: map[string]string{
				"X-Auth-Token": "the-first-parameter",
				"apiVersion":   "v1",
			},
		},
		{
			client: &mockHTTPClient{responseBody: response, statusCode: statusCode},
			Servers: []string{
				"http://server3.example.com/metrics/",
				"http://server4.example.com/metrics/",
			},
			Name:   "other_webapp",
			Method: "POST",
			Parameters: map[string]string{
				"httpParam1": "12",
				"httpParam2": "the second parameter",
			},
			Headers: map[string]string{
				"X-Auth-Token": "the-first-parameter",
				"apiVersion":   "v1",
			},
			TagKeys: []string{
				"role",
				"build",
			},
		},
	}
}

// Test that the proper values are ignored or collected
func TestHttpJson200(t *testing.T) {
	httpjson := genMockHTTPJSON(validJSON, 200)

	for _, service := range httpjson {
		var acc testutil.Accumulator
		err := acc.GatherError(service.Gather)
		require.NoError(t, err)
		require.Equal(t, 12, acc.NFields())
		// Set responsetime
		for _, p := range acc.Metrics {
			p.Fields["response_time"] = 1.0
		}

		for _, srv := range service.Servers {
			tags := map[string]string{"server": srv}
			mname := "httpjson_" + service.Name
			expectedFields["response_time"] = 1.0
			acc.AssertContainsTaggedFields(t, mname, expectedFields, tags)
		}
	}
}

// Test that GET Parameters from the url string are applied properly
func TestHttpJsonGET_URL(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		key := r.FormValue("api_key")
		require.Equal(t, "mykey", key)
		w.WriteHeader(http.StatusOK)
		_, err := fmt.Fprintln(w, validJSON2)
		require.NoError(t, err)
	}))
	defer ts.Close()

	a := HTTPJSON{
		Servers: []string{ts.URL + "?api_key=mykey"},
		Name:    "",
		Method:  "GET",
		client:  &RealHTTPClient{client: &http.Client{}},
	}

	var acc testutil.Accumulator
	err := acc.GatherError(a.Gather)
	require.NoError(t, err)

	// remove response_time from gathered fields because it's non-deterministic
	delete(acc.Metrics[0].Fields, "response_time")

	fields := map[string]interface{}{
		"market_btc_usd":                  float64(422.852),
		"market_ltc_btc":                  float64(0.00798),
		"market_ltc_cny":                  float64(21.3883),
		"market_ltc_eur":                  float64(3.113),
		"market_ltc_gbp":                  float64(2.32807),
		"market_ltc_rub":                  float64(241.796),
		"market_ltc_usd":                  float64(3.37801),
		"network_block_number":            float64(944895),
		"network_difficulty":              float64(51825.72835216),
		"network_hash_rate":               float64(1.426117703e+09),
		"network_next_difficulty":         float64(51916.15249019),
		"network_retarget_time":           float64(95053),
		"network_time_per_block":          float64(156),
		"pool_active_users":               float64(843),
		"pool_hash_rate":                  float64(1.141e+08),
		"pool_pps_rate":                   float64(7.655e-09),
		"pool_pps_ratio":                  float64(1.04),
		"user_blocks_found":               float64(0),
		"user_expected_24h_rewards":       float64(0),
		"user_hash_rate":                  float64(0),
		"user_paid_rewards":               float64(0),
		"user_past_24h_rewards":           float64(0),
		"user_total_rewards":              float64(0.000595109232),
		"user_unpaid_rewards":             float64(0.000595109232),
		"workers_brminer.1_hash_rate":     float64(0),
		"workers_brminer.1_hash_rate_24h": float64(0),
		"workers_brminer.1_reset_time":    float64(1.45540995e+09),
		"workers_brminer.1_rewards":       float64(4.5506464e-05),
		"workers_brminer.1_rewards_24h":   float64(0),
		"workers_brminer.2_hash_rate":     float64(0),
		"workers_brminer.2_hash_rate_24h": float64(0),
		"workers_brminer.2_reset_time":    float64(1.455936726e+09),
		"workers_brminer.2_rewards":       float64(0),
		"workers_brminer.2_rewards_24h":   float64(0),
		"workers_brminer.3_hash_rate":     float64(0),
		"workers_brminer.3_hash_rate_24h": float64(0),
		"workers_brminer.3_reset_time":    float64(1.455936733e+09),
		"workers_brminer.3_rewards":       float64(0),
		"workers_brminer.3_rewards_24h":   float64(0),
	}

	acc.AssertContainsFields(t, "httpjson", fields)
}

// Test that GET Parameters are applied properly
func TestHttpJsonGET(t *testing.T) {
	params := map[string]string{
		"api_key": "mykey",
	}
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		key := r.FormValue("api_key")
		require.Equal(t, "mykey", key)
		w.WriteHeader(http.StatusOK)
		_, err := fmt.Fprintln(w, validJSON2)
		require.NoError(t, err)
	}))
	defer ts.Close()

	a := HTTPJSON{
		Servers:    []string{ts.URL},
		Name:       "",
		Method:     "GET",
		Parameters: params,
		client:     &RealHTTPClient{client: &http.Client{}},
	}

	var acc testutil.Accumulator
	err := acc.GatherError(a.Gather)
	require.NoError(t, err)

	// remove response_time from gathered fields because it's non-deterministic
	delete(acc.Metrics[0].Fields, "response_time")

	fields := map[string]interface{}{
		"market_btc_usd":                  float64(422.852),
		"market_ltc_btc":                  float64(0.00798),
		"market_ltc_cny":                  float64(21.3883),
		"market_ltc_eur":                  float64(3.113),
		"market_ltc_gbp":                  float64(2.32807),
		"market_ltc_rub":                  float64(241.796),
		"market_ltc_usd":                  float64(3.37801),
		"network_block_number":            float64(944895),
		"network_difficulty":              float64(51825.72835216),
		"network_hash_rate":               float64(1.426117703e+09),
		"network_next_difficulty":         float64(51916.15249019),
		"network_retarget_time":           float64(95053),
		"network_time_per_block":          float64(156),
		"pool_active_users":               float64(843),
		"pool_hash_rate":                  float64(1.141e+08),
		"pool_pps_rate":                   float64(7.655e-09),
		"pool_pps_ratio":                  float64(1.04),
		"user_blocks_found":               float64(0),
		"user_expected_24h_rewards":       float64(0),
		"user_hash_rate":                  float64(0),
		"user_paid_rewards":               float64(0),
		"user_past_24h_rewards":           float64(0),
		"user_total_rewards":              float64(0.000595109232),
		"user_unpaid_rewards":             float64(0.000595109232),
		"workers_brminer.1_hash_rate":     float64(0),
		"workers_brminer.1_hash_rate_24h": float64(0),
		"workers_brminer.1_reset_time":    float64(1.45540995e+09),
		"workers_brminer.1_rewards":       float64(4.5506464e-05),
		"workers_brminer.1_rewards_24h":   float64(0),
		"workers_brminer.2_hash_rate":     float64(0),
		"workers_brminer.2_hash_rate_24h": float64(0),
		"workers_brminer.2_reset_time":    float64(1.455936726e+09),
		"workers_brminer.2_rewards":       float64(0),
		"workers_brminer.2_rewards_24h":   float64(0),
		"workers_brminer.3_hash_rate":     float64(0),
		"workers_brminer.3_hash_rate_24h": float64(0),
		"workers_brminer.3_reset_time":    float64(1.455936733e+09),
		"workers_brminer.3_rewards":       float64(0),
		"workers_brminer.3_rewards_24h":   float64(0),
	}

	acc.AssertContainsFields(t, "httpjson", fields)
}

// Test that POST Parameters are applied properly
func TestHttpJsonPOST(t *testing.T) {
	params := map[string]string{
		"api_key": "mykey",
	}
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		body, err := io.ReadAll(r.Body)
		require.NoError(t, err)
		require.Equal(t, "api_key=mykey", string(body))
		w.WriteHeader(http.StatusOK)
		_, err = fmt.Fprintln(w, validJSON2)
		require.NoError(t, err)
	}))
	defer ts.Close()

	a := HTTPJSON{
		Servers:    []string{ts.URL},
		Name:       "",
		Method:     "POST",
		Parameters: params,
		client:     &RealHTTPClient{client: &http.Client{}},
	}

	var acc testutil.Accumulator
	err := acc.GatherError(a.Gather)
	require.NoError(t, err)

	// remove response_time from gathered fields because it's non-deterministic
	delete(acc.Metrics[0].Fields, "response_time")

	fields := map[string]interface{}{
		"market_btc_usd":                  float64(422.852),
		"market_ltc_btc":                  float64(0.00798),
		"market_ltc_cny":                  float64(21.3883),
		"market_ltc_eur":                  float64(3.113),
		"market_ltc_gbp":                  float64(2.32807),
		"market_ltc_rub":                  float64(241.796),
		"market_ltc_usd":                  float64(3.37801),
		"network_block_number":            float64(944895),
		"network_difficulty":              float64(51825.72835216),
		"network_hash_rate":               float64(1.426117703e+09),
		"network_next_difficulty":         float64(51916.15249019),
		"network_retarget_time":           float64(95053),
		"network_time_per_block":          float64(156),
		"pool_active_users":               float64(843),
		"pool_hash_rate":                  float64(1.141e+08),
		"pool_pps_rate":                   float64(7.655e-09),
		"pool_pps_ratio":                  float64(1.04),
		"user_blocks_found":               float64(0),
		"user_expected_24h_rewards":       float64(0),
		"user_hash_rate":                  float64(0),
		"user_paid_rewards":               float64(0),
		"user_past_24h_rewards":           float64(0),
		"user_total_rewards":              float64(0.000595109232),
		"user_unpaid_rewards":             float64(0.000595109232),
		"workers_brminer.1_hash_rate":     float64(0),
		"workers_brminer.1_hash_rate_24h": float64(0),
		"workers_brminer.1_reset_time":    float64(1.45540995e+09),
		"workers_brminer.1_rewards":       float64(4.5506464e-05),
		"workers_brminer.1_rewards_24h":   float64(0),
		"workers_brminer.2_hash_rate":     float64(0),
		"workers_brminer.2_hash_rate_24h": float64(0),
		"workers_brminer.2_reset_time":    float64(1.455936726e+09),
		"workers_brminer.2_rewards":       float64(0),
		"workers_brminer.2_rewards_24h":   float64(0),
		"workers_brminer.3_hash_rate":     float64(0),
		"workers_brminer.3_hash_rate_24h": float64(0),
		"workers_brminer.3_reset_time":    float64(1.455936733e+09),
		"workers_brminer.3_rewards":       float64(0),
		"workers_brminer.3_rewards_24h":   float64(0),
	}

	acc.AssertContainsFields(t, "httpjson", fields)
}

// Test response to HTTP 500
func TestHttpJson500(t *testing.T) {
	httpjson := genMockHTTPJSON(validJSON, 500)

	var acc testutil.Accumulator
	err := acc.GatherError(httpjson[0].Gather)

	require.Error(t, err)
	require.Equal(t, 0, acc.NFields())
}

// Test response to HTTP 405
func TestHttpJsonBadMethod(t *testing.T) {
	httpjson := genMockHTTPJSON(validJSON, 200)
	httpjson[0].Method = "NOT_A_REAL_METHOD"

	var acc testutil.Accumulator
	err := acc.GatherError(httpjson[0].Gather)

	require.Error(t, err)
	require.Equal(t, 0, acc.NFields())
}

// Test response to malformed JSON
func TestHttpJsonBadJson(t *testing.T) {
	httpjson := genMockHTTPJSON(invalidJSON, 200)

	var acc testutil.Accumulator
	err := acc.GatherError(httpjson[0].Gather)

	require.Error(t, err)
	require.Equal(t, 0, acc.NFields())
}

// Test response to empty string as response object
func TestHttpJsonEmptyResponse(t *testing.T) {
	httpjson := genMockHTTPJSON(empty, 200)

	var acc testutil.Accumulator
	err := acc.GatherError(httpjson[0].Gather)
	require.NoError(t, err)
}

// Test that the proper values are ignored or collected
func TestHttpJson200Tags(t *testing.T) {
	httpjson := genMockHTTPJSON(validJSONTags, 200)

	for _, service := range httpjson {
		if service.Name == "other_webapp" {
			var acc testutil.Accumulator
			err := acc.GatherError(service.Gather)
			// Set responsetime
			for _, p := range acc.Metrics {
				p.Fields["response_time"] = 1.0
			}
			require.NoError(t, err)
			require.Equal(t, 4, acc.NFields())
			for _, srv := range service.Servers {
				tags := map[string]string{"server": srv, "role": "master", "build": "123"}
				fields := map[string]interface{}{"value": float64(15), "response_time": float64(1)}
				mname := "httpjson_" + service.Name
				acc.AssertContainsTaggedFields(t, mname, fields, tags)
			}
		}
	}
}

const validJSONArrayTags = `
[
	{
		"value": 15,
		"role": "master",
		"build": "123"
	},
	{
		"value": 17,
		"role": "slave",
		"build": "456"
	}
]`

// Test that array data is collected correctly
func TestHttpJsonArray200Tags(t *testing.T) {
	httpjson := genMockHTTPJSON(validJSONArrayTags, 200)

	for _, service := range httpjson {
		if service.Name == "other_webapp" {
			var acc testutil.Accumulator
			err := acc.GatherError(service.Gather)
			// Set responsetime
			for _, p := range acc.Metrics {
				p.Fields["response_time"] = 1.0
			}
			require.NoError(t, err)
			require.Equal(t, 8, acc.NFields())
			require.Equal(t, uint64(4), acc.NMetrics())

			for _, m := range acc.Metrics {
				if m.Tags["role"] == "master" {
					require.Equal(t, "123", m.Tags["build"])
					require.Equal(t, float64(15), m.Fields["value"])
					require.Equal(t, float64(1), m.Fields["response_time"])
					require.Equal(t, "httpjson_"+service.Name, m.Measurement)
				} else if m.Tags["role"] == "slave" {
					require.Equal(t, "456", m.Tags["build"])
					require.Equal(t, float64(17), m.Fields["value"])
					require.Equal(t, float64(1), m.Fields["response_time"])
					require.Equal(t, "httpjson_"+service.Name, m.Measurement)
				} else {
					require.FailNow(t, "unknown metric")
				}
			}
		}
	}
}

var jsonBOM = []byte("\xef\xbb\xbf[{\"value\":17}]")

// TestHttpJsonBOM tests that UTF-8 JSON with a BOM can be parsed
func TestHttpJsonBOM(t *testing.T) {
	httpjson := genMockHTTPJSON(string(jsonBOM), 200)

	for _, service := range httpjson {
		if service.Name == "other_webapp" {
			var acc testutil.Accumulator
			err := acc.GatherError(service.Gather)
			require.NoError(t, err)
		}
	}
}
