package s3client

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

// newTestAWSS3Client builds an AWSS3Client pointed at an in-test HTTP server.
// The server must answer the requests the client issues; the AWS SDK signs
// them, but the test handler does not verify signatures.
func newTestAWSS3Client(t *testing.T, handler http.HandlerFunc) *AWSS3Client {
	t.Helper()
	srv := httptest.NewServer(http.HandlerFunc(handler))
	t.Cleanup(srv.Close)

	c, err := New(context.Background(), AWSConfig{
		Region:          "us-east-1",
		Bucket:          "test-bucket",
		Endpoint:        srv.URL,
		ForcePathStyle:  true,
		AccessKeyID:     "test-access-key",
		SecretAccessKey: "test-secret-key",
	})
	if err != nil {
		t.Fatalf("New AWS client: %v", err)
	}
	return c
}

// operationCounter reads the current value of the aggregate service operation
// counter for the given labels. Prometheus text format omits zero counters, so
// a missing line reads as 0.
func operationCounter(t *testing.T, component, operation, result string) int64 {
	t.Helper()
	prefix := fmt.Sprintf(`drive9_service_operations_total{component=%q,operation=%q,result=%q} `, component, operation, result)
	text := readS3Metrics()
	idx := strings.Index(text, prefix)
	if idx < 0 {
		return 0
	}
	rest := text[idx+len(prefix):]
	if end := strings.IndexByte(rest, '\n'); end >= 0 {
		rest = rest[:end]
	}
	var v int64
	if _, err := fmt.Sscanf(rest, "%d", &v); err != nil {
		t.Fatalf("parse counter %q: %v", prefix, err)
	}
	return v
}

func writeS3Error(w http.ResponseWriter, status int, code, message string) {
	w.Header().Set("Content-Type", "application/xml")
	w.WriteHeader(status)
	_, _ = fmt.Fprintf(w,
		`<?xml version="1.0" encoding="UTF-8"?><Error><Code>%s</Code><Message>%s</Message><RequestId>req-1</RequestId><HostId>host-1</HostId></Error>`,
		code, message)
}

const listMultipartUploadsResponse = `<?xml version="1.0" encoding="UTF-8"?>
<ListMultipartUploadsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Bucket>test-bucket</Bucket>
  <KeyMarker></KeyMarker>
  <UploadIdMarker></UploadIdMarker>
  <MaxUploads>1000</MaxUploads>
  <IsTruncated>false</IsTruncated>
  <Upload>
    <Key>blobs/old-upload</Key>
    <UploadId>upid-1</UploadId>
    <Initiator><ID>initiator</ID><DisplayName>initiator</DisplayName></Initiator>
    <Owner><ID>owner</ID><DisplayName>owner</DisplayName></Owner>
    <StorageClass>STANDARD</StorageClass>
    <Initiated>2026-08-20T00:00:00.000Z</Initiated>
  </Upload>
</ListMultipartUploadsResult>`

const emptyListObjectsV2Response = `<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Name>test-bucket</Name>
  <Prefix></Prefix>
  <KeyCount>0</KeyCount>
  <MaxKeys>1000</MaxKeys>
  <IsTruncated>false</IsTruncated>
</ListBucketResult>`

// TestAWSAbortMultipartUploadNoSuchUploadIsSuccess: COS/S3 return NoSuchUpload
// when the multipart upload was already completed or aborted. drive9's abort
// flows are best-effort cleanup, so this must be treated as success: nil
// error and no error metric.
func TestAWSAbortMultipartUploadNoSuchUploadIsSuccess(t *testing.T) {
	var sawAbort bool
	c := newTestAWSS3Client(t, func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodDelete && r.URL.Query().Has("uploadId") {
			sawAbort = true
			writeS3Error(w, http.StatusNotFound, "NoSuchUpload", "The specified multipart upload does not exist")
			return
		}
		t.Errorf("unexpected request: %s %s", r.Method, r.URL)
	})

	before := operationCounter(t, "s3client", "abort_multipart_upload", "ok")
	beforeErr := operationCounter(t, "s3client", "abort_multipart_upload", "error")

	if err := c.AbortMultipartUpload(context.Background(), "blobs/x", "upid-1"); err != nil {
		t.Fatalf("AbortMultipartUpload(NoSuchUpload) error = %v, want nil", err)
	}
	if !sawAbort {
		t.Fatal("expected DELETE abort request to be issued")
	}
	if got := operationCounter(t, "s3client", "abort_multipart_upload", "ok"); got != before+1 {
		t.Fatalf("abort ok counter = %d, want %d", got, before+1)
	}
	if got := operationCounter(t, "s3client", "abort_multipart_upload", "error"); got != beforeErr {
		t.Fatalf("abort error counter = %d, want %d (NoSuchUpload must not count as error)", got, beforeErr)
	}
}

// TestAWSAbortMultipartUploadPlain404StillErrors: only the NoSuchUpload code
// is benign. A 404 carrying a different code (e.g. NotFound) must still
// surface as an error and count in the error metric, so a future
// "simplify to HTTP 404 == success" change cannot silently regress the
// classifier.
func TestAWSAbortMultipartUploadPlain404StillErrors(t *testing.T) {
	c := newTestAWSS3Client(t, func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodDelete && r.URL.Query().Has("uploadId") {
			writeS3Error(w, http.StatusNotFound, "NotFound", "The requested resource was not found")
			return
		}
		t.Errorf("unexpected request: %s %s", r.Method, r.URL)
	})

	beforeErr := operationCounter(t, "s3client", "abort_multipart_upload", "error")

	err := c.AbortMultipartUpload(context.Background(), "blobs/x", "upid-1")
	if err == nil {
		t.Fatal("AbortMultipartUpload(404 NotFound) error = nil, want error")
	}
	if !strings.Contains(err.Error(), "NotFound") {
		t.Fatalf("error = %q, want NotFound mention", err)
	}
	if got := operationCounter(t, "s3client", "abort_multipart_upload", "error"); got != beforeErr+1 {
		t.Fatalf("abort error counter = %d, want %d", got, beforeErr+1)
	}
}

// TestAWSAbortMultipartUploadOtherErrorPropagates: any abort error other than
// NoSuchUpload must still surface and count as an error metric.
func TestAWSAbortMultipartUploadOtherErrorPropagates(t *testing.T) {
	c := newTestAWSS3Client(t, func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodDelete && r.URL.Query().Has("uploadId") {
			writeS3Error(w, http.StatusForbidden, "AccessDenied", "Access Denied")
			return
		}
		t.Errorf("unexpected request: %s %s", r.Method, r.URL)
	})

	beforeErr := operationCounter(t, "s3client", "abort_multipart_upload", "error")

	err := c.AbortMultipartUpload(context.Background(), "blobs/x", "upid-1")
	if err == nil {
		t.Fatal("AbortMultipartUpload(AccessDenied) error = nil, want error")
	}
	if !strings.Contains(err.Error(), "AccessDenied") {
		t.Fatalf("error = %q, want AccessDenied mention", err)
	}
	if got := operationCounter(t, "s3client", "abort_multipart_upload", "error"); got != beforeErr+1 {
		t.Fatalf("abort error counter = %d, want %d", got, beforeErr+1)
	}
}

// TestAWSDeletePrefixNoSuchUploadAbortContinues: DeletePrefix must not fail
// when an in-progress multipart upload it tries to abort is already gone.
func TestAWSDeletePrefixNoSuchUploadAbortContinues(t *testing.T) {
	c := newTestAWSS3Client(t, func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.Method == http.MethodGet && r.URL.Query().Has("uploads"):
			w.Header().Set("Content-Type", "application/xml")
			_, _ = w.Write([]byte(listMultipartUploadsResponse))
		case r.Method == http.MethodDelete && r.URL.Query().Has("uploadId"):
			writeS3Error(w, http.StatusNotFound, "NoSuchUpload", "The specified multipart upload does not exist")
		case r.Method == http.MethodGet && r.URL.Query().Get("list-type") == "2":
			w.Header().Set("Content-Type", "application/xml")
			_, _ = w.Write([]byte(emptyListObjectsV2Response))
		default:
			t.Errorf("unexpected request: %s %s", r.Method, r.URL)
		}
	})

	res, err := c.DeletePrefix(context.Background(), "blobs/")
	if err != nil {
		t.Fatalf("DeletePrefix error = %v, want nil", err)
	}
	if res.AbortedMultipartUploads != 0 {
		t.Fatalf("AbortedMultipartUploads = %d, want 0 (already-gone uploads are not aborted)", res.AbortedMultipartUploads)
	}
}

// TestAWSDeletePrefixAbortRealErrorFails: a non-NoSuchUpload abort failure
// during prefix cleanup must fail the whole DeletePrefix.
func TestAWSDeletePrefixAbortRealErrorFails(t *testing.T) {
	c := newTestAWSS3Client(t, func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.Method == http.MethodGet && r.URL.Query().Has("uploads"):
			w.Header().Set("Content-Type", "application/xml")
			_, _ = w.Write([]byte(listMultipartUploadsResponse))
		case r.Method == http.MethodDelete && r.URL.Query().Has("uploadId"):
			writeS3Error(w, http.StatusForbidden, "AccessDenied", "Access Denied")
		default:
			t.Errorf("unexpected request: %s %s", r.Method, r.URL)
		}
	})

	_, err := c.DeletePrefix(context.Background(), "blobs/")
	if err == nil {
		t.Fatal("DeletePrefix error = nil, want error")
	}
	if !strings.Contains(err.Error(), "abort multipart upload") {
		t.Fatalf("error = %q, want abort multipart upload mention", err)
	}
}
