package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"sort"
	"strings"
	"testing"
	"time"

	"go-mod.ewintr.nl/planner/item"
)

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

	apiKey := "test"
	srv := NewServer(NewMemory(), apiKey, slog.New(slog.NewJSONHandler(os.Stdout, nil)))

	for _, tc := range []struct {
		name      string
		key       string
		url       string
		method    string
		expStatus int
	}{
		{
			name:      "index always visible",
			url:       "/",
			method:    http.MethodGet,
			expStatus: http.StatusOK,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			req, err := http.NewRequest(tc.method, tc.url, nil)
			if err != nil {
				t.Errorf("exp nil, got %v", err)
			}
			res := httptest.NewRecorder()
			srv.ServeHTTP(res, req)
			if res.Result().StatusCode != tc.expStatus {
				t.Errorf("exp %v, got %v", tc.expStatus, res.Result().StatusCode)
			}
		})
	}
}

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

	now := time.Now()
	mem := NewMemory()

	items := []item.Item{
		{ID: "id-0", Kind: item.KindEvent, Updated: now.Add(-10 * time.Minute)},
		{ID: "id-1", Kind: item.KindEvent, Updated: now.Add(-5 * time.Minute)},
		{ID: "id-2", Kind: item.KindTask, Updated: now.Add(time.Minute)},
	}

	for _, item := range items {
		if err := mem.Update(item); err != nil {
			t.Errorf("exp nil, got %v", err)
		}
	}

	apiKey := "test"
	srv := NewServer(mem, apiKey, slog.New(slog.NewJSONHandler(os.Stdout, nil)))

	for _, tc := range []struct {
		name      string
		ts        time.Time
		ks        []string
		expStatus int
		expItems  []item.Item
	}{
		{
			name:      "full",
			expStatus: http.StatusOK,
			expItems:  items,
		},
		{
			name:      "new",
			ts:        now.Add(-6 * time.Minute),
			expStatus: http.StatusOK,
			expItems:  []item.Item{items[1], items[2]},
		},
		{
			name:      "kind",
			ks:        []string{string(item.KindTask)},
			expStatus: http.StatusOK,
			expItems:  []item.Item{items[2]},
		},
		{
			name:      "unknown kind",
			ks:        []string{"test"},
			expStatus: http.StatusBadRequest,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			url := fmt.Sprintf("/sync?ts=%s", url.QueryEscape(tc.ts.Format(time.RFC3339)))
			if len(tc.ks) > 0 {
				url = fmt.Sprintf("%s&ks=%s", url, strings.Join(tc.ks, ","))
			}
			req, err := http.NewRequest(http.MethodGet, url, nil)
			if err != nil {
				t.Errorf("exp nil, got %v", err)
			}
			req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
			res := httptest.NewRecorder()
			srv.ServeHTTP(res, req)

			if res.Result().StatusCode != tc.expStatus {
				t.Errorf("exp %v, got %v", tc.expStatus, res.Result().StatusCode)
			}
			if tc.expStatus != http.StatusOK {
				return
			}

			var actItems []item.Item
			actBody, err := io.ReadAll(res.Result().Body)
			if err != nil {
				t.Errorf("exp nil, got %v", err)
			}
			defer res.Result().Body.Close()

			if err := json.Unmarshal(actBody, &actItems); err != nil {
				t.Errorf("exp nil, got %v", err)
			}

			if len(actItems) != len(tc.expItems) {
				t.Errorf("exp %d, got %d", len(tc.expItems), len(actItems))
			}
			sort.Slice(actItems, func(i, j int) bool {
				return actItems[i].ID < actItems[j].ID
			})
			for i := range actItems {
				if actItems[i].ID != tc.expItems[i].ID {
					t.Errorf("exp %v, got %v", tc.expItems[i].ID, actItems[i].ID)
				}
			}
		})
	}
}

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

	apiKey := "test"
	for _, tc := range []struct {
		name      string
		reqBody   []byte
		expStatus int
		expItems  []item.Item
	}{
		{
			name:      "empty",
			expStatus: http.StatusBadRequest,
		},
		{
			name:      "invalid json",
			reqBody:   []byte(`{"fail}`),
			expStatus: http.StatusBadRequest,
		},
		{
			name: "invalid item",
			reqBody: []byte(`[
  {"id":"id-1","kind":"event","updated":"2024-09-06T08:00:00Z"},
]`),
			expStatus: http.StatusBadRequest,
		},
		{
			name: "normal",
			reqBody: []byte(`[
  {"id":"id-1","kind":"event","updated":"2024-09-06T08:00:00Z","deleted":false,"body":"item"},
  {"id":"id-2","kind":"event","updated":"2024-09-06T08:12:00Z","deleted":false,"body":"item2"}
]`),
			expStatus: http.StatusNoContent,
			expItems: []item.Item{
				{ID: "id-1", Kind: item.KindEvent, Updated: time.Date(2024, 9, 6, 8, 0, 0, 0, time.UTC)},
				{ID: "id-2", Kind: item.KindEvent, Updated: time.Date(2024, 9, 6, 12, 0, 0, 0, time.UTC)},
			},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			mem := NewMemory()
			srv := NewServer(mem, apiKey, slog.New(slog.NewJSONHandler(os.Stdout, nil)))
			req, err := http.NewRequest(http.MethodPost, "/sync", bytes.NewBuffer(tc.reqBody))
			if err != nil {
				t.Errorf("exp nil, got %v", err)
			}
			req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
			res := httptest.NewRecorder()
			srv.ServeHTTP(res, req)

			if res.Result().StatusCode != tc.expStatus {
				t.Errorf("exp %v, got %v", tc.expStatus, res.Result().StatusCode)
			}

			actItems, err := mem.Updated([]item.Kind{}, time.Time{})
			if err != nil {
				t.Errorf("exp nil, git %v", err)
			}
			if len(actItems) != len(tc.expItems) {
				t.Errorf("exp %d, got %d", len(tc.expItems), len(actItems))
			}
			sort.Slice(actItems, func(i, j int) bool {
				return actItems[i].ID < actItems[j].ID
			})
			for i := range actItems {
				if actItems[i].ID != tc.expItems[i].ID {
					t.Errorf("exp %v, got %v", tc.expItems[i].ID, actItems[i].ID)
				}
			}
		})
	}
}