package dialectsfcrm

import (
	"context"
	"encoding/json"
	"net/http"
	"strings"
	"testing"

	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestRecycleBinListNeutralHandlerDefaultScopeAndFullShape(t *testing.T) {
	recycleBin := &stubRecycleBinContract{
		listResult: &contract.RecycleBinListResult{
			Items: []contract.RecycleBinItem{{
				ID:               "bin-1",
				RecordID:         "rec-1",
				RecordName:       "Acme Inc",
				ObjectAPIName:    "Account",
				DeletedBy:        "Alice",
				DeletedDate:      "2026-05-29T10:11:12Z",
				CreatedDate:      "2026-05-29T10:11:13Z",
				LastModifiedDate: "2026-05-29T10:11:14Z",
			}},
			FailedObjects: []contract.RecycleBinFailure{{
				ObjectAPIName: "Opportunity",
				ErrorCode:     "Forbidden",
				Message:       "no FLS",
			}},
		},
	}

	resp, err := RecycleBinListNeutralHandler(context.Background(), &dialect.Request{
		User: &models.UserSession{ID: "u1"},
	}, dialect.Deps{Contracts: contract.Deps{RecycleBin: recycleBin}})
	if err != nil {
		t.Fatalf("handler error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200", resp.Status)
	}
	if recycleBin.listScope != contract.RecycleBinScopeMine {
		t.Fatalf("scope = %q, want %q", recycleBin.listScope, contract.RecycleBinScopeMine)
	}

	body := mustRecycleBinJSONMap(t, resp.Body)
	for _, want := range []string{"items", "totalSize", "partialFailure", "failedObjects"} {
		if _, ok := body[want]; !ok {
			t.Fatalf("response missing %q: %#v", want, body)
		}
	}
	if body["totalSize"].(float64) != 1 {
		t.Fatalf("totalSize = %#v, want 1", body["totalSize"])
	}
	if body["partialFailure"] != true {
		t.Fatalf("partialFailure = %#v, want true", body["partialFailure"])
	}
	items := body["items"].([]any)
	item := items[0].(map[string]any)
	for _, want := range []string{"Id", "RecordId", "RecordName", "ObjectApiName", "DeletedBy", "DeletedDate", "CreatedDate", "LastModifiedDate"} {
		if _, ok := item[want]; !ok {
			t.Fatalf("item missing %q: %#v", want, item)
		}
	}
}

func TestRecycleBinListNeutralHandlerInvalidScopeRendersV63Array(t *testing.T) {
	resp, err := RecycleBinListNeutralHandler(context.Background(), &dialect.Request{
		Query: map[string][]string{"scope": {"everyone"}},
	}, dialect.Deps{Contracts: contract.Deps{RecycleBin: &stubRecycleBinContract{
		listErr: statusError{status: http.StatusBadRequest, message: `unsupported scope "everyone"`},
	}}})
	if err != nil {
		t.Fatalf("handler error: %v", err)
	}
	assertRecycleBinSFError(t, resp, http.StatusBadRequest, wire.SFCodeInvalidRequest, `unsupported scope "everyone"`)
}

func TestRecycleBinErrorResponseDoesNotLeakInternalMessages(t *testing.T) {
	resp := recycleBinErrorResponse(&dialect.Request{Version: "v63.0"}, newInternalConditionError("db secret: SELECT password FROM users"))
	if resp.Status != http.StatusInternalServerError {
		t.Fatalf("status = %d, want 500", resp.Status)
	}
	body := mustRecycleBinJSON(t, resp.Body)
	if strings.Contains(body, "SELECT") || strings.Contains(body, "password") || strings.Contains(body, "db secret") {
		t.Fatalf("5xx body leaks internal text: %s", body)
	}
	assertRecycleBinSFError(t, resp, http.StatusInternalServerError, wire.SFCodeUnknownException, recycleBinGenericInternalErrorMessage)
}

func TestRecycleBinRestoreAndPurgeNeutralHandlers(t *testing.T) {
	recycleBin := &stubRecycleBinContract{}
	deps := dialect.Deps{Contracts: contract.Deps{RecycleBin: recycleBin}}

	restoreResp, err := RecycleBinRestoreNeutralHandler(context.Background(), &dialect.Request{
		Params: map[string]string{"id": "rec-1"},
		User:   &models.UserSession{ID: "u1"},
	}, deps)
	if err != nil {
		t.Fatalf("restore handler error: %v", err)
	}
	if restoreResp.Status != http.StatusNoContent {
		t.Fatalf("restore status = %d, want 204", restoreResp.Status)
	}
	if recycleBin.restoreID != "rec-1" {
		t.Fatalf("restoreID = %q, want rec-1", recycleBin.restoreID)
	}

	purgeResp, err := RecycleBinPurgeNeutralHandler(context.Background(), &dialect.Request{
		Params: map[string]string{"id": "rec-2"},
		User:   &models.UserSession{ID: "u1"},
	}, deps)
	if err != nil {
		t.Fatalf("purge handler error: %v", err)
	}
	if purgeResp.Status != http.StatusNoContent {
		t.Fatalf("purge status = %d, want 204", purgeResp.Status)
	}
	if recycleBin.purgeID != "rec-2" {
		t.Fatalf("purgeID = %q, want rec-2", recycleBin.purgeID)
	}
}

func TestRecycleBinRestoreMissingIDRendersInvalidRequest(t *testing.T) {
	resp, err := RecycleBinRestoreNeutralHandler(context.Background(), &dialect.Request{
		Params: map[string]string{"id": "  "},
	}, dialect.Deps{Contracts: contract.Deps{RecycleBin: &stubRecycleBinContract{}}})
	if err != nil {
		t.Fatalf("handler error: %v", err)
	}
	assertRecycleBinSFError(t, resp, http.StatusBadRequest, wire.SFCodeInvalidRequest, "record id is required")
}

func TestRecycleBinDomainNotFoundRendersV63NotFound(t *testing.T) {
	resp := recycleBinErrorResponse(&dialect.Request{Version: "v63.0"}, contractNotFoundError())
	assertRecycleBinSFError(t, resp, http.StatusNotFound, wire.SFCodeNotFound, "missing tombstone")
}

type stubRecycleBinContract struct {
	listScope  contract.RecycleBinScope
	listResult *contract.RecycleBinListResult
	listErr    error
	restoreID  string
	restoreErr error
	purgeID    string
	purgeErr   error
}

func (s *stubRecycleBinContract) ListRecycleBin(_ context.Context, _ *models.UserSession, scope contract.RecycleBinScope) (*contract.RecycleBinListResult, error) {
	s.listScope = scope
	return s.listResult, s.listErr
}

func (s *stubRecycleBinContract) RestoreRecycleBinRecord(_ context.Context, recordID string, _ *models.UserSession) error {
	s.restoreID = recordID
	return s.restoreErr
}

func (s *stubRecycleBinContract) PurgeRecycleBinRecord(_ context.Context, recordID string, _ *models.UserSession) error {
	s.purgeID = recordID
	return s.purgeErr
}

type statusError struct {
	status  int
	message string
}

func (e statusError) Error() string   { return e.message }
func (e statusError) HTTPStatus() int { return e.status }

func contractNotFoundError() error {
	return contractNotFound{message: "missing tombstone"}
}

type contractNotFound struct {
	message string
}

func (e contractNotFound) Error() string { return e.message }
func (e contractNotFound) NotFoundCondition() bool {
	return true
}

func assertRecycleBinSFError(t *testing.T, resp *dialect.Response, wantStatus int, wantCode string, wantMessage string) {
	t.Helper()
	if resp.Status != wantStatus {
		t.Fatalf("status = %d, want %d body=%s", resp.Status, wantStatus, mustRecycleBinJSON(t, resp.Body))
	}
	var arr []wire.SFError
	if err := json.Unmarshal([]byte(mustRecycleBinJSON(t, resp.Body)), &arr); err != nil {
		t.Fatalf("response body is not v63 error array: %v body=%#v", err, resp.Body)
	}
	if len(arr) != 1 {
		t.Fatalf("error array length = %d, want 1", len(arr))
	}
	if arr[0].ErrorCode != wantCode || arr[0].Message != wantMessage {
		t.Fatalf("error = %#v, want code=%q message=%q", arr[0], wantCode, wantMessage)
	}
}

func mustRecycleBinJSONMap(t *testing.T, v any) map[string]any {
	t.Helper()
	var out map[string]any
	if err := json.Unmarshal([]byte(mustRecycleBinJSON(t, v)), &out); err != nil {
		t.Fatalf("body is not object JSON: %v", err)
	}
	return out
}

func mustRecycleBinJSON(t *testing.T, v any) string {
	t.Helper()
	b, err := json.Marshal(v)
	if err != nil {
		t.Fatalf("marshal body: %v", err)
	}
	return string(b)
}
