package recordattachmentsv1

import (
	"context"
	"errors"
	"runtime"
	"strings"
	"testing"
	"time"
)

func testContext(t *testing.T) context.Context {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	t.Cleanup(cancel)
	return ctx
}

func validRequest() ResolveRequest {
	return ResolveRequest{
		ParentObjectID: "WorkItem", ParentRecordID: "record-1", ResolverID: "document_publication",
		Sources: []Source{{SourceFieldID: "DocumentLink", SourceValue: "ABCDEF123456", SourceValueOffset: 42}},
	}
}

func validResponse() ResolveResponse {
	return ResolveResponse{
		Results:  []SourceResult{{Index: 0, Outcome: SourceResolved, FileID: "file-1", FileName: "example.pdf"}},
		Receipts: Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName},
	}
}

func TestRequestAndResponseBindEachSourceToOneExactFieldProof(t *testing.T) {
	ctx := testContext(t)
	request := validRequest()
	if err := ValidateRequest(ctx, request); err != nil {
		t.Fatal(err)
	}
	if err := ValidateResponse(request, validResponse()); err != nil {
		t.Fatal(err)
	}
}

func TestRequestPreservesWhitespaceAsPartOfTheExactSourceValue(t *testing.T) {
	request := validRequest()
	request.Sources[0].SourceValue = " source token "
	if err := ValidateRequest(testContext(t), request); err != nil {
		t.Fatalf("exact source value was narrowed by semantic validation: %v", err)
	}
}

func TestRequestRejectsMissingOrMalformedFieldProof(t *testing.T) {
	ctx := testContext(t)
	for name, mutate := range map[string]func(*ResolveRequest){
		"no sources": func(request *ResolveRequest) { request.Sources = nil },
		"no field":   func(request *ResolveRequest) { request.Sources[0].SourceFieldID = "" },
		"no value":   func(request *ResolveRequest) { request.Sources[0].SourceValue = "" },
	} {
		t.Run(name, func(t *testing.T) {
			request := validRequest()
			mutate(&request)
			var failure *Failure
			if err := ValidateRequest(ctx, request); !errors.As(err, &failure) || failure.Category != FailureInvalidRequest {
				t.Fatalf("failure = %#v", err)
			}
		})
	}
}

func TestRequestRejectsOneOverSourceBound(t *testing.T) {
	request := validRequest()
	request.Sources = make([]Source, MaxSources+1)
	for index := range request.Sources {
		request.Sources[index] = Source{SourceFieldID: "DocumentLink", SourceValue: "ABCDEF123456", SourceValueOffset: uint32(index)}
	}
	var failure *Failure
	if err := ValidateRequest(testContext(t), request); !errors.As(err, &failure) || failure.Category != FailureResourceExhausted || failure.LimitKind != LimitOperationCount {
		t.Fatalf("failure = %#v", err)
	}
}

func TestOversizedSourceValueIsRejectedBeforeProportionalAllocation(t *testing.T) {
	request := validRequest()
	request.Sources[0].SourceValue = strings.Repeat("x", MaxSemanticRequestBytes*128)
	assertResourceExhausted(t, ValidateRequest(testContext(t), request), LimitSemanticRequest)

	runtime.GC()
	var before, after runtime.MemStats
	runtime.ReadMemStats(&before)
	_ = ValidateRequest(testContext(t), request)
	runtime.ReadMemStats(&after)
	if allocated := after.TotalAlloc - before.TotalAlloc; allocated > uint64(MaxSemanticRequestBytes*16) {
		t.Fatalf("oversized request validation allocated %d bytes; input-size-proportional allocation occurred", allocated)
	}
}

func TestOversizedResponseFileNameIsRejectedBeforeProportionalAllocation(t *testing.T) {
	response := validResponse()
	response.Results[0].FileName = strings.Repeat("x", MaxSemanticResponseBytes*16)
	assertResourceExhausted(t, ValidateResponse(validRequest(), response), LimitSemanticResponse)

	runtime.GC()
	var before, after runtime.MemStats
	runtime.ReadMemStats(&before)
	_ = ValidateResponse(validRequest(), response)
	runtime.ReadMemStats(&after)
	if allocated := after.TotalAlloc - before.TotalAlloc; allocated > uint64(MaxSemanticResponseBytes) {
		t.Fatalf("oversized response validation allocated %d bytes; input-size-proportional allocation occurred", allocated)
	}
}

func TestResponseRequiresExactInputOrderAndOpaqueNonResolution(t *testing.T) {
	request := validRequest()
	request.Sources = append(request.Sources, Source{SourceFieldID: "OtherLink", SourceValue: "123456ABCDEF", SourceValueOffset: 9})
	response := validResponse()
	response.Results = append(response.Results, SourceResult{Index: 1, Outcome: SourceUnavailable})
	if err := ValidateResponse(request, response); err != nil {
		t.Fatal(err)
	}
	response.Results[1] = SourceResult{Index: 1, Outcome: SourceUnavailable, FileID: "leak"}
	if err := ValidateResponse(request, response); err == nil {
		t.Fatal("unavailable result leaked file data")
	}
	response.Results[1] = SourceResult{Index: 0, Outcome: SourceNotFound}
	if err := ValidateResponse(request, response); err == nil {
		t.Fatal("reordered result was accepted")
	}
}

func assertResourceExhausted(t *testing.T, err error, limit LimitKind) {
	t.Helper()
	var failure *Failure
	if !errors.As(err, &failure) || failure.Category != FailureResourceExhausted || failure.LimitKind != limit {
		t.Fatalf("failure = %#v", err)
	}
}
