package querycodec

import (
	"context"
	"net/http"
	"net/url"
	"testing"
	"time"
)

func testDateContext(t *testing.T, instant, zone string) DateContext {
	t.Helper()
	now, err := time.Parse(time.RFC3339, instant)
	if err != nil {
		t.Fatal(err)
	}
	loc, err := time.LoadLocation(zone)
	if err != nil {
		t.Fatal(err)
	}
	return DateContext{Now: now, Location: loc,
		FieldType: func(_ context.Context, object, field string) (string, error) {
			if field == "CreatedDate" {
				return "DateTime", nil
			}
			if field == "Name" {
				return "Text", nil
			}
			return "Date", nil
		},
		ChildObject: func(context.Context, string, string) (string, error) { return "Child", nil },
	}
}

func TestRelativeDateCalendarRanges(t *testing.T) {
	for _, tc := range []struct{ name, field, literal, now, zone, low, high, upper string }{
		{"date today", "CloseDate", "TODAY", "2026-09-08T00:30:00Z", "America/Los_Angeles", "2026-09-07", "2026-09-08", "<"},
		{"DST spring 23 hours", "CreatedDate", "TODAY", "2026-03-08T12:00:00Z", "America/Los_Angeles", "2026-03-08T08:00:00Z", "2026-03-09T07:00:00Z", "<"},
		{"DST autumn 25 hours", "CreatedDate", "TODAY", "2026-11-01T12:00:00Z", "America/Los_Angeles", "2026-11-01T07:00:00Z", "2026-11-02T08:00:00Z", "<"},
		{"year local", "CreatedDate", "THIS_YEAR", "2026-01-01T00:30:00Z", "America/Los_Angeles", "2025-01-01T08:00:00Z", "2026-01-01T08:00:00Z", "<"},
		{"leap year", "CloseDate", "THIS_YEAR", "2024-02-29T12:00:00Z", "UTC", "2024-01-01", "2025-01-01", "<"},
		{"days includes today", "CloseDate", "LAST_N_DAYS:30", "2026-09-08T12:34:56Z", "UTC", "2026-08-09", "2026-09-09", "<"},
		{"days current second", "CreatedDate", "LAST_N_DAYS:30", "2026-09-08T12:34:56Z", "UTC", "2026-08-09T00:00:00Z", "2026-09-08T12:34:56Z", "<="},
		{"current instant preserves precision", "CreatedDate", "LAST_N_DAYS:1", "2026-09-08T12:34:56.9Z", "UTC", "2026-09-07T00:00:00Z", "2026-09-08T12:34:56.9Z", "<="},
		{"zero days", "CloseDate", "LAST_N_DAYS:0", "2026-09-08T12:00:00Z", "UTC", "2026-09-08", "2026-09-09", "<"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			q, err := ParseSOQL("SELECT Id FROM Item WHERE " + tc.field + " = " + tc.literal)
			if err != nil {
				t.Fatal(err)
			}
			if err = ResolveRelativeDates(context.Background(), q, testDateContext(t, tc.now, tc.zone), nil); err != nil {
				t.Fatal(err)
			}
			p := q.Filter
			if p.Kind != PredicateAnd || len(p.Children) != 2 {
				t.Fatalf("range = %#v", p)
			}
			low, high := p.Children[0], p.Children[1]
			text := func(o Operand) string {
				switch v := o.Value.(type) {
				case DateLiteralValue:
					return string(v)
				case DateTimeLiteralValue:
					return string(v)
				}
				t.Fatalf("not a typed temporal literal: %#v", o)
				return ""
			}
			if low.Op != ">=" || high.Op != tc.upper || text(low.Right) != tc.low || text(high.Right) != tc.high {
				t.Fatalf("range = %#v; %#v", low, high)
			}
		})
	}
}

func TestRelativeDateComparisonOperators(t *testing.T) {
	for _, tc := range []struct {
		op   string
		kind PredicateKind
		ops  string
	}{
		{"=", PredicateAnd, ">=,<"}, {"!=", PredicateOr, "<,>="}, {">", PredicateComparison, ">="}, {">=", PredicateComparison, ">="}, {"<", PredicateComparison, "<"}, {"<=", PredicateComparison, "<"},
	} {
		t.Run(tc.op, func(t *testing.T) {
			q, err := ParseSOQL("SELECT Id FROM Item WHERE CloseDate " + tc.op + " TODAY")
			if err != nil {
				t.Fatal(err)
			}
			if err = ResolveRelativeDates(context.Background(), q, testDateContext(t, "2026-09-08T12:00:00Z", "UTC"), nil); err != nil {
				t.Fatal(err)
			}
			p := q.Filter
			ops := p.Op
			if len(p.Children) > 0 {
				ops = p.Children[0].Op + "," + p.Children[1].Op
			}
			if p.Kind != tc.kind || ops != tc.ops {
				t.Fatalf("predicate=%#v", p)
			}
		})
	}
}

func TestRelativeDateSyntaxAndPlacement(t *testing.T) {
	for _, literal := range []string{"LAST_N_DAYS", "LAST_N_DAYS:", "LAST_N_DAYS:-1", "LAST_N_DAYS:1.5", "LAST_N_DAYS:999999999999999999999"} {
		if _, err := ParseSOQL("SELECT Id FROM Item WHERE CloseDate = " + literal); err == nil {
			t.Errorf("accepted %s", literal)
		}
	}
	for _, filter := range []string{"Name = TODAY", "CloseDate IN (TODAY)", "TODAY = CloseDate"} {
		q, err := ParseSOQL("SELECT Id FROM Item WHERE " + filter)
		if err != nil {
			continue
		}
		if err = ResolveRelativeDates(context.Background(), q, testDateContext(t, "2026-09-08T12:00:00Z", "UTC"), nil); err == nil {
			t.Errorf("accepted %s", filter)
		}
	}
	q, err := ParseSOQL("SELECT Id FROM Item WHERE Name = 'TODAY'")
	if err != nil {
		t.Fatal(err)
	}
	if hasRelativeDates(q) || q.Filter.Right.Value != "TODAY" {
		t.Fatalf("quoted string reinterpreted: %#v", q.Filter)
	}
	q, err = ParseSOQLWith("SELECT Id, (SELECT Id FROM Children WHERE CloseDate = TODAY) FROM Item WHERE Id IN (SELECT ParentId FROM Child WHERE CloseDate = THIS_YEAR)", ParseOpts{AllowSemiJoin: true})
	if err != nil {
		t.Fatal(err)
	}
	if err = ResolveRelativeDates(context.Background(), q, testDateContext(t, "2026-09-08T12:00:00Z", "UTC"), nil); err != nil {
		t.Fatal(err)
	}
	if hasRelativeDates(q) {
		t.Fatal("nested literal was not resolved")
	}
}

func TestRelativeDateQueryMoreKeepsOriginalCalendar(t *testing.T) {
	calendar := testDateContext(t, "2026-09-08T23:59:59Z", "UTC")
	var predicates []Predicate
	handler, err := NewHandler(Executor{
		Dates: func(ctx context.Context, q *QueryRequest, anchor *DateAnchor) (context.Context, error) {
			return ctx, ResolveRelativeDates(ctx, q, calendar, anchor)
		},
		Query: func(_ context.Context, q QueryRequest, _ int) (Result, error) {
			predicates = append(predicates, q.Filter)
			return Result{TotalSize: 201, Done: len(predicates) > 1, Records: []Record{{ObjectType: "Item", Fields: map[string]any{"Id": "one"}}}}, nil
		},
		History: func(context.Context, HistoryRequest) (HistoryResult, error) { return HistoryResult{}, nil },
	})
	if err != nil {
		t.Fatal(err)
	}
	first := handler.Handle(context.Background(), Request{Route: Route{Kind: RouteQuery, Version: "v63.0"}, Query: url.Values{"q": {"SELECT Id FROM Item WHERE CloseDate = TODAY"}}, Header: http.Header{"Sforce-Query-Options": {"batchSize=200"}}})
	if first.Status != 200 {
		t.Fatalf("first=%#v", first)
	}
	next, ok := first.Body.(map[string]any)["nextRecordsUrl"].(string)
	if !ok {
		t.Fatalf("missing continuation: %#v", first)
	}
	calendar.Now = calendar.Now.Add(2 * time.Second)
	route, ok := Match(next)
	if !ok {
		t.Fatal(next)
	}
	second := handler.Handle(context.Background(), Request{Route: route})
	if second.Status != 200 {
		t.Fatalf("second=%#v", second)
	}
	if len(predicates) != 2 || predicates[0].Children[0].Right.Value != predicates[1].Children[0].Right.Value {
		t.Fatalf("calendar drifted: %#v", predicates)
	}
}
