package service

import (
	"bytes"
	"compress/gzip"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/fleetdm/fleet/v4/pkg/fleethttp"
	"github.com/fleetdm/fleet/v4/server/fleet"
	"github.com/fleetdm/fleet/v4/server/ptr"
	"github.com/go-kit/kit/log"
	"github.com/go-kit/kit/log/level"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"
)

func TestIntegrationLoggerTestSuite(t *testing.T) {
	testingSuite := new(integrationLoggerTestSuite)
	testingSuite.s = &testingSuite.Suite
	suite.Run(t, testingSuite)
}

type integrationLoggerTestSuite struct {
	withServer
	suite.Suite

	buf *bytes.Buffer
}

func (s *integrationLoggerTestSuite) SetupSuite() {
	s.withDS.SetupSuite("integrationLoggerTestSuite")

	s.buf = new(bytes.Buffer)
	logger := log.NewJSONLogger(s.buf)
	logger = level.NewFilter(logger, level.AllowDebug())

	users, server := RunServerForTestsWithDS(s.T(), s.ds, TestServerOpts{Logger: logger})
	s.server = server
	s.users = users
}

func (s *integrationLoggerTestSuite) TearDownTest() {
	s.buf.Reset()
}

func (s *integrationLoggerTestSuite) TestLogger() {
	t := s.T()

	s.token = getTestAdminToken(t, s.server)

	s.getConfig()

	params := fleet.QueryPayload{
		Name:        ptr.String("somequery"),
		Description: ptr.String("desc"),
		Query:       ptr.String("select 1 from osquery;"),
	}
	var createResp createQueryResponse
	s.DoJSON("POST", "/api/v1/fleet/queries", params, http.StatusOK, &createResp)

	logs := s.buf.String()
	parts := strings.Split(strings.TrimSpace(logs), "\n")
	assert.Len(t, parts, 3)
	for i, part := range parts {
		kv := make(map[string]string)
		err := json.Unmarshal([]byte(part), &kv)
		require.NoError(t, err)

		assert.NotEqual(t, "", kv["took"])

		switch i {
		case 0:
			assert.Equal(t, "info", kv["level"])
			assert.Equal(t, "POST", kv["method"])
			assert.Equal(t, "/api/v1/fleet/login", kv["uri"])
		case 1:
			assert.Equal(t, "debug", kv["level"])
			assert.Equal(t, "GET", kv["method"])
			assert.Equal(t, "/api/v1/fleet/config", kv["uri"])
			assert.Equal(t, "admin1@example.com", kv["user"])
		case 2:
			assert.Equal(t, "debug", kv["level"])
			assert.Equal(t, "POST", kv["method"])
			assert.Equal(t, "/api/v1/fleet/queries", kv["uri"])
			assert.Equal(t, "admin1@example.com", kv["user"])
			assert.Equal(t, "somequery", kv["name"])
			assert.Equal(t, "select 1 from osquery;", kv["sql"])
		default:
			t.Fail()
		}
	}
}

func (s *integrationLoggerTestSuite) TestOsqueryEndpointsLogErrors() {
	t := s.T()

	_, err := s.ds.NewHost(context.Background(), &fleet.Host{
		DetailUpdatedAt: time.Now(),
		LabelUpdatedAt:  time.Now(),
		PolicyUpdatedAt: time.Now(),
		SeenTime:        time.Now(),
		NodeKey:         t.Name() + "1234",
		UUID:            "1",
		Hostname:        "foo.local",
		OsqueryHostID:   t.Name(),
		PrimaryIP:       "192.168.1.1",
		PrimaryMac:      "30-65-EC-6F-C4-58",
	})
	require.NoError(t, err)

	requestBody := io.NopCloser(bytes.NewBuffer([]byte(`{"node_key":"1234","log_type":"status","data":[}`)))
	req, _ := http.NewRequest("POST", s.server.URL+"/api/v1/osquery/log", requestBody)
	client := fleethttp.NewClient()
	_, err = client.Do(req)
	require.Nil(t, err)

	logString := s.buf.String()
	assert.Contains(t, logString, `invalid character '}' looking for beginning of value","level":"info","path":"/api/v1/osquery/log"}
`, logString)
}

func (s *integrationLoggerTestSuite) TestSubmitLog() {
	t := s.T()

	h, err := s.ds.NewHost(context.Background(), &fleet.Host{
		DetailUpdatedAt: time.Now(),
		LabelUpdatedAt:  time.Now(),
		PolicyUpdatedAt: time.Now(),
		SeenTime:        time.Now(),
		NodeKey:         t.Name() + "1234",
		UUID:            "1",
		Hostname:        "foo.local",
		PrimaryIP:       "192.168.1.1",
		PrimaryMac:      "30-65-EC-6F-C4-58",
		OsqueryHostID:   t.Name(),
	})
	require.NoError(t, err)

	// submit status logs
	req := submitLogsRequest{
		NodeKey: h.NodeKey,
		LogType: "status",
		Data:    nil,
	}
	res := submitLogsResponse{}
	s.DoJSON("POST", "/api/v1/osquery/log", req, http.StatusOK, &res)

	logString := s.buf.String()
	assert.Equal(t, 1, strings.Count(logString, `"ip_addr"`))
	assert.Equal(t, 1, strings.Count(logString, "x_for_ip_addr"))
	s.buf.Reset()

	// submit results logs
	req = submitLogsRequest{
		NodeKey: h.NodeKey,
		LogType: "result",
		Data:    nil,
	}
	res = submitLogsResponse{}
	s.DoJSON("POST", "/api/v1/osquery/log", req, http.StatusOK, &res)

	logString = s.buf.String()
	assert.Equal(t, 1, strings.Count(logString, `"ip_addr"`))
	assert.Equal(t, 1, strings.Count(logString, "x_for_ip_addr"))
	s.buf.Reset()

	// submit invalid type logs
	req = submitLogsRequest{
		NodeKey: h.NodeKey,
		LogType: "unknown",
		Data:    nil,
	}
	var errRes map[string]string
	s.DoJSON("POST", "/api/v1/osquery/log", req, http.StatusInternalServerError, &errRes)
	assert.Contains(t, errRes["error"], "unknown log type")
	s.buf.Reset()

	// submit gzip-encoded request
	var body bytes.Buffer
	gw := gzip.NewWriter(&body)
	_, err = fmt.Fprintf(gw, `{
		"node_key": %q,
		"log_type": "status",
		"data":     null
	}`, h.NodeKey)
	require.NoError(t, err)
	require.NoError(t, gw.Close())

	s.DoRawWithHeaders("POST", "/api/v1/osquery/log", body.Bytes(), http.StatusOK, map[string]string{"Content-Encoding": "gzip"})
	logString = s.buf.String()
	assert.Equal(t, 1, strings.Count(logString, `"ip_addr"`))
	assert.Equal(t, 1, strings.Count(logString, "x_for_ip_addr"))

	// submit same payload without specifying gzip encoding fails
	s.DoRawWithHeaders("POST", "/api/v1/osquery/log", body.Bytes(), http.StatusInternalServerError, nil)
}

func (s *integrationLoggerTestSuite) TestEnrollAgentLogsErrors() {
	t := s.T()
	_, err := s.ds.NewHost(context.Background(), &fleet.Host{
		DetailUpdatedAt: time.Now(),
		LabelUpdatedAt:  time.Now(),
		PolicyUpdatedAt: time.Now(),
		SeenTime:        time.Now(),
		NodeKey:         "1234",
		UUID:            "1",
		Hostname:        "foo.local",
		PrimaryIP:       "192.168.1.1",
		PrimaryMac:      "30-65-EC-6F-C4-58",
	})
	require.NoError(t, err)

	j, err := json.Marshal(&enrollAgentRequest{
		EnrollSecret:   "1234",
		HostIdentifier: "4321",
		HostDetails:    nil,
	})
	require.NoError(t, err)

	s.DoRawNoAuth("POST", "/api/v1/osquery/enroll", j, http.StatusUnauthorized)

	parts := strings.Split(strings.TrimSpace(s.buf.String()), "\n")
	require.Len(t, parts, 1)
	logData := make(map[string]json.RawMessage)
	require.NoError(t, json.Unmarshal([]byte(parts[0]), &logData))
	assert.Equal(t, `"error"`, string(logData["level"]))
	assert.Contains(t, string(logData["err"]), `"enroll failed:`)
	assert.Contains(t, string(logData["err"]), `no matching secret found`)
}
