package chislog_test

import (
	"bytes"
	"encoding/json"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"gitea.dwysokinski.me/twhelp/corev3/internal/chislog"
	"github.com/go-chi/chi/v5"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestLogger(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name        string
		register    func(r chi.Router)
		options     []chislog.Option
		req         func(t *testing.T) *http.Request
		assertEntry func(t *testing.T, entry map[string]any)
		excluded    bool
	}{
		{
			name: "log level should be Info when status code >= 200 and < 400",
			register: func(r chi.Router) {
				r.Get("/info", func(w http.ResponseWriter, r *http.Request) {
					w.WriteHeader(http.StatusOK)
				})
			},
			req: func(t *testing.T) *http.Request {
				t.Helper()
				return httptest.NewRequest(http.MethodGet, "/info?test=true", nil)
			},
			assertEntry: func(t *testing.T, entry map[string]any) {
				t.Helper()
				assert.Equal(t, slog.LevelInfo.String(), entry[slog.LevelKey])
				assert.Equal(t, "192.0.2.1", entry["httpRequest.ip"]) // default ip address set by httptest.NewRequest
			},
		},
		{
			name: "log level should be Warn when status code >= 400 and < 500",
			register: func(r chi.Router) {
				r.Get("/warn", func(w http.ResponseWriter, r *http.Request) {
					w.WriteHeader(http.StatusBadRequest)
				})
			},
			req: func(t *testing.T) *http.Request {
				t.Helper()
				return httptest.NewRequest(http.MethodGet, "/warn?test=true", nil)
			},
			assertEntry: func(t *testing.T, entry map[string]any) {
				t.Helper()
				assert.Equal(t, slog.LevelWarn.String(), entry[slog.LevelKey])
			},
		},
		{
			name: "log level should be Error when status code >= 500",
			register: func(r chi.Router) {
				r.Get("/error", func(w http.ResponseWriter, r *http.Request) {
					w.WriteHeader(http.StatusInternalServerError)
				})
			},
			req: func(t *testing.T) *http.Request {
				t.Helper()
				return httptest.NewRequest(http.MethodGet, "/error?test=true", nil)
			},
			assertEntry: func(t *testing.T, entry map[string]any) {
				t.Helper()
				assert.Equal(t, slog.LevelError.String(), entry[slog.LevelKey])
			},
		},
		{
			name: "read IP from header X-Forwarded-For",
			options: []chislog.Option{
				chislog.WithIPExtractor(func(r *http.Request) string {
					return r.Header.Get("X-Forwarded-For")
				}),
			},
			register: func(r chi.Router) {
				r.Post("/x-forwarded-for", func(w http.ResponseWriter, r *http.Request) {
					w.WriteHeader(http.StatusOK)
				})
			},
			req: func(t *testing.T) *http.Request {
				t.Helper()
				req := httptest.NewRequest(http.MethodPost, "/x-forwarded-for", nil)
				req.Header.Set("X-Forwarded-For", "111.111.111.111")
				return req
			},
			assertEntry: func(t *testing.T, entry map[string]any) {
				t.Helper()
				assert.Equal(t, slog.LevelInfo.String(), entry[slog.LevelKey])
				assert.Equal(t, "111.111.111.111", entry["httpRequest.ip"])
			},
		},
		{
			name: "log entry should be skipped",
			options: []chislog.Option{
				chislog.WithFilter(func(r *http.Request) bool {
					return !strings.HasPrefix(r.URL.Path, "/meta")
				}),
			},
			register: func(r chi.Router) {
				r.Get("/meta/test", func(w http.ResponseWriter, r *http.Request) {
					w.WriteHeader(http.StatusOK)
				})
			},
			req: func(t *testing.T) *http.Request {
				t.Helper()
				return httptest.NewRequest(http.MethodGet, "/meta/test", nil)
			},
			excluded: true,
		},
	}

	for _, tt := range tests {
		tt := tt

		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			buf := bytes.NewBuffer(nil)
			logger := slog.New(slog.NewJSONHandler(buf, nil))

			r := chi.NewRouter()
			r.Use(chislog.Logger(logger, tt.options...))
			tt.register(r)

			rr := httptest.NewRecorder()
			req := tt.req(t)

			r.ServeHTTP(rr, req)

			if tt.excluded {
				assert.Zero(t, buf.Bytes())
				return
			}

			var entry map[string]any
			require.NoError(t, json.Unmarshal(buf.Bytes(), &entry))
			assert.NotEmpty(t, entry["time"])
			assert.Equal(t, req.Method, entry["httpRequest.method"])
			assert.Equal(t, req.RequestURI, entry["httpRequest.uri"])
			assert.Equal(t, req.Referer(), entry["httpRequest.referer"])
			assert.Equal(t, req.UserAgent(), entry["httpRequest.userAgent"])
			assert.Equal(t, req.Proto, entry["httpRequest.proto"])
			assert.Equal(t, float64(rr.Code), entry["httpResponse.status"])
			assert.GreaterOrEqual(t, entry["httpResponse.bytes"], 0.0)
			assert.GreaterOrEqual(t, entry["httpResponse.duration"], 0.0)
			if tt.assertEntry != nil {
				tt.assertEntry(t, entry)
			}
		})
	}
}
