package audit

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
)

func TestExtractClientIP(t *testing.T) {
	tests := []struct {
		name       string
		remoteAddr string
		setupFunc  func() http.Header
		want       string
	}{
		{
			name:       "from RemoteAddr with port",
			remoteAddr: "192.168.1.100:12345",
			setupFunc:  func() http.Header { return http.Header{} },
			want:       "192.168.1.100",
		},
		{
			name:       "from RemoteAddr without port",
			remoteAddr: "192.168.1.100",
			setupFunc:  func() http.Header { return http.Header{} },
			want:       "192.168.1.100",
		},
		{
			name:       "from X-Forwarded-For single IP",
			remoteAddr: "10.0.0.1:8080",
			setupFunc: func() http.Header {
				h := http.Header{}
				h.Set("X-Forwarded-For", "203.0.113.50")
				return h
			},
			want: "203.0.113.50",
		},
		{
			name:       "from X-Forwarded-For multiple IPs",
			remoteAddr: "10.0.0.1:8080",
			setupFunc: func() http.Header {
				h := http.Header{}
				h.Set("X-Forwarded-For", "203.0.113.50, 70.41.3.18, 150.172.238.178")
				return h
			},
			want: "203.0.113.50",
		},
		{
			name:       "from X-Real-IP",
			remoteAddr: "10.0.0.1:8080",
			setupFunc: func() http.Header {
				h := http.Header{}
				h.Set("X-Real-IP", "198.51.100.25")
				return h
			},
			want: "198.51.100.25",
		},
		{
			name:       "X-Forwarded-For takes precedence over X-Real-IP",
			remoteAddr: "10.0.0.1:8080",
			setupFunc: func() http.Header {
				h := http.Header{}
				h.Set("X-Forwarded-For", "203.0.113.50")
				h.Set("X-Real-IP", "198.51.100.25")
				return h
			},
			want: "203.0.113.50",
		},
		{
			name:       "IPv6 address",
			remoteAddr: "[::1]:8080",
			setupFunc:  func() http.Header { return http.Header{} },
			want:       "::1",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := &http.Request{
				RemoteAddr: tt.remoteAddr,
				Header:     tt.setupFunc(),
			}
			result := ExtractClientIP(req)
			assert.Equal(t, tt.want, result)
		})
	}
}

func TestRecordRequest_WritesEntry(t *testing.T) {
	logger := &Logger{
		entryCh: make(chan *Entry, 1),
		hot:     newHotConfig(&Config{MaxBodySize: 256}),
	}

	body := `{"password":"secret","name":"alice"}`
	req := httptest.NewRequest(http.MethodPost, "/path?token=abc&foo=bar", strings.NewReader(body))

	logger.RecordRequest(req, RequestRecordOptions{
		Start:        time.Now(),
		StatusCode:   http.StatusOK,
		OriginalPath: req.URL.Path,
		RawQuery:     req.URL.RawQuery,
		Body:         []byte(body),
	})

	select {
	case entry := <-logger.entryCh:
		if entry.RequestMethod != http.MethodPost {
			t.Fatalf("expected method %s, got %s", http.MethodPost, entry.RequestMethod)
		}
		if entry.RequestURI != "/path" {
			t.Fatalf("expected uri /path, got %s", entry.RequestURI)
		}
		if entry.RequestParams != "foo=bar&token=%5BREDACTED%5D" {
			t.Fatalf("unexpected query params: %s", entry.RequestParams)
		}
		var bodyMap map[string]interface{}
		if err := json.Unmarshal([]byte(entry.RequestBody), &bodyMap); err != nil {
			t.Fatalf("failed to unmarshal body: %v", err)
		}
		if bodyMap["password"] != redactedValue {
			t.Fatalf("expected password redacted, got %v", bodyMap["password"])
		}
		if bodyMap["name"] != "alice" {
			t.Fatalf("expected name alice, got %v", bodyMap["name"])
		}
	default:
		t.Fatal("expected audit entry to be logged")
	}
}

func TestRecordRequest_SkipGetWhenDisabled(t *testing.T) {
	logger := &Logger{
		entryCh: make(chan *Entry, 1),
		hot:     newHotConfig(&Config{IncludeGetRequests: false}),
	}
	req := httptest.NewRequest(http.MethodGet, "/path", nil)

	logger.RecordRequest(req, RequestRecordOptions{
		Start:      time.Now(),
		StatusCode: http.StatusOK,
	})

	select {
	case <-logger.entryCh:
		t.Fatal("did not expect audit entry for GET when disabled")
	default:
	}
}

func TestRecordRequest_DoesNotLogBodyOnErrorStatus(t *testing.T) {
	logger := &Logger{
		entryCh: make(chan *Entry, 1),
		hot:     newHotConfig(&Config{MaxBodySize: 256}),
	}

	body := `{"password":"secret","name":"alice"}`
	req := httptest.NewRequest(http.MethodPost, "/path", strings.NewReader(body))

	logger.RecordRequest(req, RequestRecordOptions{
		Start:      time.Now(),
		StatusCode: http.StatusBadRequest,
		Body:       []byte(body),
	})

	select {
	case entry := <-logger.entryCh:
		assert.Equal(t, "", entry.RequestBody)
	default:
		t.Fatal("expected audit entry to be logged")
	}
}

func TestRecordRequest_DoesNotLogBodyWhenMaxBodySizeDisabled(t *testing.T) {
	logger := &Logger{
		entryCh: make(chan *Entry, 1),
		hot:     newHotConfig(&Config{MaxBodySize: 0}),
	}

	body := `{"password":"secret","name":"alice"}`
	req := httptest.NewRequest(http.MethodPost, "/path", strings.NewReader(body))

	logger.RecordRequest(req, RequestRecordOptions{
		Start:      time.Now(),
		StatusCode: http.StatusOK,
		Body:       []byte(body),
	})

	select {
	case entry := <-logger.entryCh:
		assert.Equal(t, "", entry.RequestBody)
	default:
		t.Fatal("expected audit entry to be logged")
	}
}
