package ingest_test

import (
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"testing"
	"time"

	bulkclient "github.com/nexuscrm/tests/contract/bulk_api_v2/client"
	"github.com/nexuscrm/tests/contract/bulk_api_v2/internal/arrange"
	"github.com/nexuscrm/tests/contract/internal/liveapi"
)

const (
	ingestObjectAPIName = "Account"
	ingestWaitTimeout   = 60 * time.Second
	jobOperationInsert  = "insert"
	jobStateOpen        = "Open"
	jobStateAborted     = "Aborted"
	jobStateUploadComplete = "UploadComplete"
	jobStateJobComplete  = "JobComplete"
	jobStateFailed       = "Failed"
)

type liveFixture struct {
	client     *liveapi.Client
	targetName string
}

type salesforceError struct {
	ErrorCode string `json:"errorCode"`
	Message   string `json:"message"`
}

func newLiveFixture(t *testing.T) *liveFixture {
	t.Helper()
	arrange.EnsureLocalStandardObjects(t)
	cfg := liveapi.LoadConfig(t)
	return &liveFixture{
		client:     liveapi.NewAuthenticatedClient(t),
		targetName: cfg.TargetName,
	}
}

func requireStatus(t *testing.T, resp *http.Response, want int) {
	t.Helper()
	if resp.StatusCode == want {
		return
	}
	body := string(liveapi.ReadBody(t, resp))
	t.Fatalf("status = %d, want %d, body=%s", resp.StatusCode, want, body)
}

func requireStatusAny(t *testing.T, resp *http.Response, wants ...int) {
	t.Helper()
	for _, want := range wants {
		if resp.StatusCode == want {
			return
		}
	}
	body := string(liveapi.ReadBody(t, resp))
	t.Fatalf("status = %d, want one of %v, body=%s", resp.StatusCode, wants, body)
}

func decodeJSON[T any](t *testing.T, resp *http.Response) T {
	t.Helper()
	var out T
	liveapi.DecodeJSON(t, resp, &out)
	return out
}

func requireSalesforceError(t *testing.T, resp *http.Response, wantStatus int, wantCode string) []salesforceError {
	t.Helper()
	if resp.StatusCode != wantStatus {
		body := string(liveapi.ReadBody(t, resp))
		t.Fatalf("status = %d, want %d, body=%s", resp.StatusCode, wantStatus, body)
	}
	var errs []salesforceError
	liveapi.DecodeJSON(t, resp, &errs)
	if len(errs) == 0 {
		t.Fatalf("expected non-empty Salesforce error array")
	}
	if wantCode != "" && errs[0].ErrorCode != wantCode {
		t.Fatalf("errorCode = %s, want %s (body=%+v)", errs[0].ErrorCode, wantCode, errs)
	}
	return errs
}

func requireSalesforceErrorAny(t *testing.T, resp *http.Response, wantStatus int, wantCodes ...string) []salesforceError {
	t.Helper()
	if resp.StatusCode != wantStatus {
		body := string(liveapi.ReadBody(t, resp))
		t.Fatalf("status = %d, want %d, body=%s", resp.StatusCode, wantStatus, body)
	}
	var errs []salesforceError
	liveapi.DecodeJSON(t, resp, &errs)
	if len(errs) == 0 {
		t.Fatalf("expected non-empty Salesforce error array")
	}
	if len(wantCodes) > 0 {
		matched := false
		for _, wantCode := range wantCodes {
			if errs[0].ErrorCode == wantCode {
				matched = true
				break
			}
		}
		if !matched {
			t.Fatalf("errorCode = %s, want one of %v (body=%+v)", errs[0].ErrorCode, wantCodes, errs)
		}
	}
	return errs
}

func uniqueName(prefix string) string {
	return fmt.Sprintf("%s-%d", prefix, time.Now().UTC().UnixNano())
}

func (f *liveFixture) createIngestJob(t *testing.T, objectAPIName string) bulkclient.BulkJob {
	t.Helper()

	resp, err := f.client.DoJSON(http.MethodPost, "/jobs/ingest", bulkclient.CreateIngestJobRequest{
		Object:    objectAPIName,
		Operation: jobOperationInsert,
	})
	if err != nil {
		t.Fatalf("create ingest job: %v", err)
	}
	if f.targetName == "sandbox" {
		requireStatusAny(t, resp, http.StatusOK, http.StatusCreated)
	} else {
		requireStatus(t, resp, http.StatusCreated)
	}
	return decodeJSON[bulkclient.BulkJob](t, resp)
}

func (f *liveFixture) getIngestJob(t *testing.T, jobID string) bulkclient.BulkJob {
	t.Helper()

	resp, err := f.client.Do(http.MethodGet, "/jobs/ingest/"+url.PathEscape(jobID), "", nil)
	if err != nil {
		t.Fatalf("get ingest job %s: %v", jobID, err)
	}
	requireStatus(t, resp, http.StatusOK)
	return decodeJSON[bulkclient.BulkJob](t, resp)
}

func (f *liveFixture) closeIngestJob(t *testing.T, jobID string, state string) bulkclient.BulkJob {
	t.Helper()

	resp, err := f.client.DoJSON(http.MethodPatch, "/jobs/ingest/"+url.PathEscape(jobID), bulkclient.UpdateJobStateRequest{
		State: state,
	})
	if err != nil {
		t.Fatalf("close ingest job %s to %s: %v", jobID, state, err)
	}
	requireStatus(t, resp, http.StatusOK)
	return decodeJSON[bulkclient.BulkJob](t, resp)
}

func (f *liveFixture) uploadIngestJobCSV(t *testing.T, jobID, csvBody string) {
	t.Helper()

	resp, err := f.client.Do(http.MethodPut, "/jobs/ingest/"+url.PathEscape(jobID)+"/batches", "text/csv", []byte(csvBody))
	if err != nil {
		t.Fatalf("upload ingest csv for %s: %v", jobID, err)
	}
	requireStatus(t, resp, http.StatusCreated)
}

func (f *liveFixture) waitForIngestJobState(t *testing.T, jobID string, want string) bulkclient.BulkJob {
	t.Helper()

	deadline := time.Now().Add(ingestWaitTimeout)
	for time.Now().Before(deadline) {
		job := f.getIngestJob(t, jobID)
		if job.State == want {
			return job
		}
		if job.State == jobStateFailed && want != jobStateFailed {
			t.Fatalf("ingest job %s failed unexpectedly: %+v", jobID, job)
		}
		time.Sleep(500 * time.Millisecond)
	}

	t.Fatalf("ingest job %s did not reach state %s in time", jobID, want)
	return bulkclient.BulkJob{}
}

func (f *liveFixture) cleanupAccountsByName(t *testing.T, names ...string) {
	t.Helper()

	for _, name := range names {
		escapedName := strings.ReplaceAll(name, "'", "\\'")
		query := "/query?q=" + url.QueryEscape("SELECT Id FROM Account WHERE Name = '"+escapedName+"'")
		resp, err := f.client.Do(http.MethodGet, query, "", nil)
		if err != nil {
			t.Fatalf("query account %q for cleanup: %v", name, err)
		}
		if resp.StatusCode != http.StatusOK {
			body := string(liveapi.ReadBody(t, resp))
			t.Fatalf("query account %q status %d body=%s", name, resp.StatusCode, body)
		}
		var payload struct {
			Records []struct {
				ID string `json:"Id"`
			} `json:"records"`
		}
		liveapi.DecodeJSON(t, resp, &payload)
		for _, record := range payload.Records {
			delResp, err := f.client.Do(http.MethodDelete, "/sobjects/Account/"+url.PathEscape(record.ID), "", nil)
			if err != nil {
				t.Fatalf("delete account %s: %v", record.ID, err)
			}
			if delResp.StatusCode != http.StatusNoContent && delResp.StatusCode != http.StatusNotFound {
				body := string(liveapi.ReadBody(t, delResp))
				t.Fatalf("delete account %s status %d body=%s", record.ID, delResp.StatusCode, body)
			}
		}
	}
}
