package app

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"strings"
	"testing"
	"time"

	eventconsumer "github.com/codex-k8s/kodex/libs/go/eventconsumer"
	eventlog "github.com/codex-k8s/kodex/libs/go/eventlog"
	providerevents "github.com/codex-k8s/kodex/libs/go/platformevents/provider"
	"github.com/google/uuid"

	"github.com/codex-k8s/kodex/services/internal/project-catalog/internal/domain/errs"
	projectservice "github.com/codex-k8s/kodex/services/internal/project-catalog/internal/domain/service"
)

func TestBootstrapMergeEventHandlerRecordsDiagnostic(t *testing.T) {
	t.Parallel()

	projectID := uuid.New()
	repositoryID := uuid.New()
	recorder := &fakeBootstrapMergeRecorder{}
	handler := bootstrapMergeEventHandler{reconciler: recorder}

	result := handler.HandleEvent(context.Background(), eventconsumer.Event{StoredEvent: bootstrapMergeStoredEvent(t, bootstrapMergePayload(projectID, repositoryID))})
	if result.Status != eventconsumer.ResultAck {
		t.Fatalf("HandleEvent() status = %s, want ack: %+v", result.Status, result)
	}
	if len(recorder.inputs) != 1 {
		t.Fatalf("recorded inputs = %d, want 1", len(recorder.inputs))
	}
	input := recorder.inputs[0]
	if input.ProjectID != projectID || input.RepositoryID != repositoryID {
		t.Fatalf("ids = %s/%s, want %s/%s", input.ProjectID, input.RepositoryID, projectID, repositoryID)
	}
	if input.MergeSignal.SignalKind != "bootstrap" || input.MergeSignal.SignalKey != "github/bootstrap/PR_123" {
		t.Fatalf("merge signal = %+v, want bootstrap signal", input.MergeSignal)
	}
	if input.MergeSignal.SourceRef != "kodex/bootstrap" || input.MergeSignal.BaseBranch != "main" {
		t.Fatalf("refs = %+v, want safe provider source and base branch", input.MergeSignal)
	}
	if input.SignalFingerprint == "" || input.ErrorCode != bootstrapMergeMissingCheckedArtifactCode {
		t.Fatalf("diagnostic = %+v, want fingerprint and missing artifact code", input)
	}
}

func TestBootstrapMergeEventHandlerRecordsDiagnosticForPartialCheckedPolicy(t *testing.T) {
	t.Parallel()

	projectID := uuid.New()
	repositoryID := uuid.New()
	recorder := &fakeBootstrapMergeRecorder{}
	handler := bootstrapMergeEventHandler{reconciler: recorder}
	payload := bootstrapMergePayload(projectID, repositoryID)
	payload.CheckedArtifactRef = "artifact://provider/bootstrap/PR_123/services.yaml"

	result := handler.HandleEvent(context.Background(), eventconsumer.Event{StoredEvent: bootstrapMergeStoredEvent(t, payload)})
	if result.Status != eventconsumer.ResultAck {
		t.Fatalf("HandleEvent() status = %s, want ack: %+v", result.Status, result)
	}
	if len(recorder.inputs) != 1 {
		t.Fatalf("recorded inputs = %d, want 1", len(recorder.inputs))
	}
	if len(recorder.reconcileInputs) != 0 {
		t.Fatalf("reconcile inputs = %d, want 0", len(recorder.reconcileInputs))
	}
	input := recorder.inputs[0]
	if input.ErrorCode != bootstrapMergeIncompleteCheckedArtifactCode {
		t.Fatalf("error code = %q, want %q", input.ErrorCode, bootstrapMergeIncompleteCheckedArtifactCode)
	}
	if input.SignalFingerprint == "" {
		t.Fatalf("signal fingerprint is empty for partial checked policy diagnostic")
	}
}

func TestBootstrapMergeEventHandlerRejectsUnsafePayload(t *testing.T) {
	t.Parallel()

	handler := bootstrapMergeEventHandler{reconciler: &fakeBootstrapMergeRecorder{}}
	storedEvent := bootstrapMergeStoredEvent(t, providerevents.Payload{})
	storedEvent.Payload = []byte(`{"project_id":`)

	result := handler.HandleEvent(context.Background(), eventconsumer.Event{StoredEvent: storedEvent})
	if result.Status != eventconsumer.ResultPoison || result.Code != "invalid_payload" {
		t.Fatalf("HandleEvent() = %+v, want invalid_payload poison", result)
	}
}

func TestBootstrapMergeEventHandlerMapsDomainErrors(t *testing.T) {
	t.Parallel()

	projectID := uuid.New()
	repositoryID := uuid.New()
	cases := []struct {
		name   string
		err    error
		status eventconsumer.ResultStatus
		code   string
	}{
		{name: "conflict", err: errs.ErrConflict, status: eventconsumer.ResultPoison, code: "conflicting_signal"},
		{name: "unknown binding", err: errs.ErrNotFound, status: eventconsumer.ResultPoison, code: "unknown_binding"},
		{name: "temporary", err: errors.New("database unavailable"), status: eventconsumer.ResultRetry, code: "retryable"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			handler := bootstrapMergeEventHandler{reconciler: &fakeBootstrapMergeRecorder{diagnosticErr: tc.err}}
			result := handler.HandleEvent(context.Background(), eventconsumer.Event{StoredEvent: bootstrapMergeStoredEvent(t, bootstrapMergePayload(projectID, repositoryID))})
			if result.Status != tc.status || result.Code != tc.code {
				t.Fatalf("HandleEvent() = %+v, want %s/%s", result, tc.status, tc.code)
			}
		})
	}
}

func TestBootstrapMergeEventHandlerReconcilesCheckedPolicy(t *testing.T) {
	t.Parallel()

	projectID := uuid.New()
	repositoryID := uuid.New()
	recorder := &fakeBootstrapMergeRecorder{}
	handler := bootstrapMergeEventHandler{reconciler: recorder}
	payload := bootstrapMergePayload(projectID, repositoryID)
	payloadWithCheckedPolicy(&payload)

	result := handler.HandleEvent(context.Background(), eventconsumer.Event{StoredEvent: bootstrapMergeStoredEvent(t, payload)})
	if result.Status != eventconsumer.ResultAck {
		t.Fatalf("HandleEvent() status = %s, want ack: %+v", result.Status, result)
	}
	if len(recorder.reconcileInputs) != 1 {
		t.Fatalf("reconcile inputs = %d, want 1", len(recorder.reconcileInputs))
	}
	if len(recorder.inputs) != 0 {
		t.Fatalf("diagnostic inputs = %d, want 0", len(recorder.inputs))
	}
	input := recorder.reconcileInputs[0]
	if input.ProjectID != projectID || input.RepositoryID != repositoryID {
		t.Fatalf("ids = %s/%s, want %s/%s", input.ProjectID, input.RepositoryID, projectID, repositoryID)
	}
	if input.MergeSignal.WatermarkDigest != bootstrapWatermarkDigest() || string(input.MergeSignal.WatermarkJSON) != bootstrapWatermarkJSON {
		t.Fatalf("watermark = %s/%s, want checked watermark", input.MergeSignal.WatermarkDigest, input.MergeSignal.WatermarkJSON)
	}
	if input.CheckedPolicy.ArtifactRef != "artifact://provider/bootstrap/PR_123/services.yaml" ||
		input.CheckedPolicy.ArtifactDigest != bootstrapCheckedContentHash() ||
		input.CheckedPolicy.ArtifactVersion != payload.MergeCommitSHA ||
		input.CheckedPolicy.SourcePath != "services.yaml" ||
		input.CheckedPolicy.ContentHash != bootstrapCheckedContentHash() ||
		string(input.CheckedPolicy.ValidatedPayload) != bootstrapValidatedPayloadJSON {
		t.Fatalf("checked policy = %+v, want checked artifact input", input.CheckedPolicy)
	}
}

func TestAdoptionMergeEventHandlerRecordsDiagnostic(t *testing.T) {
	t.Parallel()

	projectID := uuid.New()
	repositoryID := uuid.New()
	recorder := &fakeBootstrapMergeRecorder{}
	handler := adoptionMergeEventHandler{reconciler: recorder}

	result := handler.HandleEvent(context.Background(), eventconsumer.Event{StoredEvent: adoptionMergeStoredEvent(t, adoptionMergePayload(projectID, repositoryID))})
	if result.Status != eventconsumer.ResultAck {
		t.Fatalf("HandleEvent() status = %s, want ack: %+v", result.Status, result)
	}
	if len(recorder.adoptionInputs) != 1 {
		t.Fatalf("recorded adoption inputs = %d, want 1", len(recorder.adoptionInputs))
	}
	input := recorder.adoptionInputs[0]
	if input.ProjectID != projectID || input.RepositoryID != repositoryID {
		t.Fatalf("ids = %s/%s, want %s/%s", input.ProjectID, input.RepositoryID, projectID, repositoryID)
	}
	if input.MergeSignal.SignalKind != "adoption" || input.MergeSignal.SignalKey != "github/adoption/PR_123" {
		t.Fatalf("merge signal = %+v, want adoption signal", input.MergeSignal)
	}
	if input.SignalFingerprint == "" || input.ErrorCode != adoptionMergeMissingCheckedArtifactCode {
		t.Fatalf("diagnostic = %+v, want fingerprint and missing artifact code", input)
	}
}

func TestAdoptionMergeEventHandlerReconcilesCheckedPolicy(t *testing.T) {
	t.Parallel()

	projectID := uuid.New()
	repositoryID := uuid.New()
	recorder := &fakeBootstrapMergeRecorder{}
	handler := adoptionMergeEventHandler{reconciler: recorder}
	payload := adoptionMergePayload(projectID, repositoryID)
	payloadWithAdoptionCheckedPolicy(&payload)

	result := handler.HandleEvent(context.Background(), eventconsumer.Event{StoredEvent: adoptionMergeStoredEvent(t, payload)})
	if result.Status != eventconsumer.ResultAck {
		t.Fatalf("HandleEvent() status = %s, want ack: %+v", result.Status, result)
	}
	if len(recorder.adoptionReconcileInputs) != 1 {
		t.Fatalf("adoption reconcile inputs = %d, want 1", len(recorder.adoptionReconcileInputs))
	}
	if len(recorder.adoptionInputs) != 0 {
		t.Fatalf("adoption diagnostic inputs = %d, want 0", len(recorder.adoptionInputs))
	}
	input := recorder.adoptionReconcileInputs[0]
	if input.ProjectID != projectID || input.RepositoryID != repositoryID {
		t.Fatalf("ids = %s/%s, want %s/%s", input.ProjectID, input.RepositoryID, projectID, repositoryID)
	}
	if input.MergeSignal.SignalKind != "adoption" || input.MergeSignal.SourceRef != "kodex/adoption" {
		t.Fatalf("merge signal = %+v, want adoption merge refs", input.MergeSignal)
	}
	if input.MergeSignal.WatermarkDigest != adoptionWatermarkDigest() || string(input.MergeSignal.WatermarkJSON) != adoptionWatermarkJSON {
		t.Fatalf("watermark = %s/%s, want checked adoption watermark", input.MergeSignal.WatermarkDigest, input.MergeSignal.WatermarkJSON)
	}
	if input.CheckedPolicy.ArtifactRef != "artifact://provider/adoption/PR_123/services.yaml" ||
		input.CheckedPolicy.ArtifactDigest != bootstrapCheckedContentHash() ||
		input.CheckedPolicy.ArtifactVersion != payload.MergeCommitSHA ||
		input.CheckedPolicy.SourcePath != "services.yaml" ||
		input.CheckedPolicy.ContentHash != bootstrapCheckedContentHash() ||
		string(input.CheckedPolicy.ValidatedPayload) != bootstrapValidatedPayloadJSON {
		t.Fatalf("checked policy = %+v, want checked adoption artifact input", input.CheckedPolicy)
	}
}

func TestAdoptionMergeEventHandlerRejectsBootstrapSignal(t *testing.T) {
	t.Parallel()

	projectID := uuid.New()
	repositoryID := uuid.New()
	handler := adoptionMergeEventHandler{reconciler: &fakeBootstrapMergeRecorder{}}

	result := handler.HandleEvent(context.Background(), eventconsumer.Event{StoredEvent: adoptionMergeStoredEvent(t, bootstrapMergePayload(projectID, repositoryID))})
	if result.Status != eventconsumer.ResultPoison || result.Code != "invalid_signal_kind" {
		t.Fatalf("HandleEvent() = %+v, want invalid_signal_kind poison", result)
	}
}

func TestBootstrapMergeEventHandlerMapsReconcileErrors(t *testing.T) {
	t.Parallel()

	projectID := uuid.New()
	repositoryID := uuid.New()
	payload := bootstrapMergePayload(projectID, repositoryID)
	payloadWithCheckedPolicy(&payload)
	handler := bootstrapMergeEventHandler{reconciler: &fakeBootstrapMergeRecorder{reconcileErr: errs.ErrConflict}}

	result := handler.HandleEvent(context.Background(), eventconsumer.Event{StoredEvent: bootstrapMergeStoredEvent(t, payload)})
	if result.Status != eventconsumer.ResultPoison || result.Code != "conflicting_signal" {
		t.Fatalf("HandleEvent() = %+v, want conflicting_signal poison", result)
	}
}

func TestBootstrapMergeEventFingerprintIgnoresDeliveryIdentity(t *testing.T) {
	t.Parallel()

	projectID := uuid.New()
	repositoryID := uuid.New()
	payload := bootstrapMergePayload(projectID, repositoryID)
	first := bootstrapMergeStoredEvent(t, payload)
	second := bootstrapMergeStoredEvent(t, payload)
	second.SequenceID = first.SequenceID + 1
	second.Event.ID = uuid.New()
	second.Event.AggregateID = uuid.New()

	firstFingerprint := bootstrapMergeEventFingerprint(first, payload)
	secondFingerprint := bootstrapMergeEventFingerprint(second, payload)
	if firstFingerprint != secondFingerprint {
		t.Fatalf("fingerprints differ for delivery replay: %s != %s", firstFingerprint, secondFingerprint)
	}

	changedPayload := payload
	changedPayload.MergeCommitSHA = "fedcba9876543210fedcba9876543210fedcba98"
	changedFingerprint := bootstrapMergeEventFingerprint(second, changedPayload)
	if changedFingerprint == firstFingerprint {
		t.Fatalf("fingerprint did not change after safe provider merge commit changed: %s", changedFingerprint)
	}
}

func bootstrapMergePayload(projectID uuid.UUID, repositoryID uuid.UUID) providerevents.Payload {
	return providerevents.Payload{
		ProviderSlug:                "github",
		RepositoryMergeSignalID:     uuid.New().String(),
		SignalKey:                   "github/bootstrap/PR_123",
		SignalKind:                  "bootstrap",
		ProjectID:                   projectID.String(),
		RepositoryID:                repositoryID.String(),
		RepositoryFullName:          "codex-k8s/kodex",
		ProviderRepositoryID:        "R_123",
		BaseBranch:                  "main",
		HeadBranch:                  "kodex/bootstrap",
		SourceRef:                   "kodex/bootstrap",
		MergeCommitSHA:              "0123456789abcdef0123456789abcdef01234567",
		WatermarkDigest:             "sha256:" + strings.Repeat("a", 64),
		WorkItemProjectionID:        "projection-1",
		PullRequestProviderID:       "PR_123",
		PullRequestURL:              "https://github.com/codex-k8s/kodex/pull/123",
		RelatedProviderOperationRef: "operation-1",
		ObservedAt:                  time.Date(2026, 5, 27, 12, 0, 0, 0, time.UTC).Format(time.RFC3339),
		MergedAt:                    time.Date(2026, 5, 27, 12, 1, 0, 0, time.UTC).Format(time.RFC3339),
		Version:                     3,
	}
}

func adoptionMergePayload(projectID uuid.UUID, repositoryID uuid.UUID) providerevents.Payload {
	payload := bootstrapMergePayload(projectID, repositoryID)
	payload.RepositoryMergeSignalID = uuid.New().String()
	payload.SignalKey = "github/adoption/PR_123"
	payload.SignalKind = "adoption"
	payload.HeadBranch = "kodex/adoption"
	payload.SourceRef = "kodex/adoption"
	return payload
}

const bootstrapValidatedPayloadJSON = `{"spec":{"services":[{"key":"api","rootPath":"services/api","kind":"backend"}]}}`
const bootstrapWatermarkJSON = `{"kind":"provider_pr","managed_by":"kodex","work_type":"repository_bootstrap","source_ref":"services.yaml"}`
const adoptionWatermarkJSON = `{"kind":"provider_pr","managed_by":"kodex","work_type":"repository_adoption","source_ref":"services.yaml"}`

func payloadWithCheckedPolicy(payload *providerevents.Payload) {
	payload.CheckedArtifactRef = "artifact://provider/bootstrap/PR_123/services.yaml"
	payload.CheckedArtifactDigest = bootstrapCheckedContentHash()
	payload.CheckedArtifactVersion = payload.MergeCommitSHA
	payload.CheckedSourcePath = "services.yaml"
	payload.CheckedContentHash = bootstrapCheckedContentHash()
	payload.CheckedValidatedPayloadJSON = bootstrapValidatedPayloadJSON
	payload.CheckedWatermarkJSON = bootstrapWatermarkJSON
	payload.WatermarkDigest = bootstrapWatermarkDigest()
}

func payloadWithAdoptionCheckedPolicy(payload *providerevents.Payload) {
	payload.CheckedArtifactRef = "artifact://provider/adoption/PR_123/services.yaml"
	payload.CheckedArtifactDigest = bootstrapCheckedContentHash()
	payload.CheckedArtifactVersion = payload.MergeCommitSHA
	payload.CheckedSourcePath = "services.yaml"
	payload.CheckedContentHash = bootstrapCheckedContentHash()
	payload.CheckedValidatedPayloadJSON = bootstrapValidatedPayloadJSON
	payload.CheckedWatermarkJSON = adoptionWatermarkJSON
	payload.WatermarkDigest = adoptionWatermarkDigest()
}

func bootstrapCheckedContentHash() string {
	return sha256Hex([]byte("spec:\n  services:\n    - key: api\n      rootPath: services/api\n      kind: backend\n"))
}

func bootstrapWatermarkDigest() string {
	return sha256Hex([]byte(bootstrapWatermarkJSON))
}

func adoptionWatermarkDigest() string {
	return sha256Hex([]byte(adoptionWatermarkJSON))
}

func sha256Hex(payload []byte) string {
	sum := sha256.Sum256(payload)
	return "sha256:" + hex.EncodeToString(sum[:])
}

func bootstrapMergeStoredEvent(t *testing.T, payload providerevents.Payload) eventlog.StoredEvent {
	t.Helper()
	payloadBytes, err := json.Marshal(payload)
	if err != nil {
		t.Fatalf("json.Marshal(): %v", err)
	}
	return eventlog.StoredEvent{
		SequenceID: 1,
		Event: eventlog.Event{
			ID:            uuid.New(),
			SourceService: "provider-hub",
			EventType:     providerevents.EventRepositoryBootstrapMerged,
			SchemaVersion: providerevents.SchemaVersion,
			AggregateType: providerevents.AggregateRepositoryMergeSignal,
			AggregateID:   uuid.New(),
			Payload:       payloadBytes,
			OccurredAt:    time.Now().UTC(),
		},
		RecordedAt: time.Now().UTC(),
	}
}

func adoptionMergeStoredEvent(t *testing.T, payload providerevents.Payload) eventlog.StoredEvent {
	t.Helper()
	event := bootstrapMergeStoredEvent(t, payload)
	event.Event.EventType = providerevents.EventRepositoryAdoptionMerged
	return event
}

type fakeBootstrapMergeRecorder struct {
	inputs                  []projectservice.BootstrapMergeSignalDiagnosticInput
	reconcileInputs         []projectservice.ReconcileBootstrapMergeSignalInput
	adoptionInputs          []projectservice.AdoptionMergeSignalDiagnosticInput
	adoptionReconcileInputs []projectservice.ReconcileAdoptionMergeSignalInput
	diagnosticErr           error
	reconcileErr            error
}

func (r *fakeBootstrapMergeRecorder) RecordBootstrapMergeSignalDiagnostic(_ context.Context, input projectservice.BootstrapMergeSignalDiagnosticInput) error {
	r.inputs = append(r.inputs, input)
	return r.diagnosticErr
}

func (r *fakeBootstrapMergeRecorder) ReconcileBootstrapMergeSignal(_ context.Context, input projectservice.ReconcileBootstrapMergeSignalInput) (projectservice.BootstrapServicesPolicyImportResult, error) {
	r.reconcileInputs = append(r.reconcileInputs, input)
	return projectservice.BootstrapServicesPolicyImportResult{}, r.reconcileErr
}

func (r *fakeBootstrapMergeRecorder) RecordAdoptionMergeSignalDiagnostic(_ context.Context, input projectservice.AdoptionMergeSignalDiagnosticInput) error {
	r.adoptionInputs = append(r.adoptionInputs, input)
	return r.diagnosticErr
}

func (r *fakeBootstrapMergeRecorder) ReconcileAdoptionMergeSignal(_ context.Context, input projectservice.ReconcileAdoptionMergeSignalInput) (projectservice.BootstrapServicesPolicyImportResult, error) {
	r.adoptionReconcileInputs = append(r.adoptionReconcileInputs, input)
	return projectservice.BootstrapServicesPolicyImportResult{}, r.reconcileErr
}
