package provider

import (
	"context"
	"reflect"
	"testing"
	"time"

	"git.toki-labs.com/toki/gito/services/core/internal/core"
)

func TestConfigNormalizedTrimsIdentityFields(t *testing.T) {
	config := Config{
		ID:            ProviderID(" GitHub "),
		Endpoint:      " https://api.github.com ",
		CredentialRef: " secret://gito/providers/github ",
		Capabilities:  CapabilityChangeRequest | CapabilityWebhook,
	}

	got := config.Normalized()

	if got.ID != ProviderGitHub {
		t.Fatalf("ID: got %q want %q", got.ID, ProviderGitHub)
	}
	if got.Endpoint != "https://api.github.com" {
		t.Fatalf("Endpoint: got %q", got.Endpoint)
	}
	if got.CredentialRef != "secret://gito/providers/github" {
		t.Fatalf("CredentialRef: got %q", got.CredentialRef)
	}
}

func TestCapabilityFlags(t *testing.T) {
	capabilities := CapabilityChangeRequest | CapabilityWebhook | CapabilityComment

	if !capabilities.Has(CapabilityChangeRequest) {
		t.Fatal("expected change request capability")
	}
	if !capabilities.Has(CapabilityWebhook) {
		t.Fatal("expected webhook capability")
	}
	if !capabilities.Has(CapabilityComment) {
		t.Fatal("expected comment capability")
	}
	if capabilities.Has(CapabilityChecks) {
		t.Fatal("did not expect checks capability")
	}
	if capabilities.Has(0) {
		t.Fatal("zero capability should not be supported")
	}
}

func TestConfigSupportsCapability(t *testing.T) {
	config := Config{
		ID:           ProviderForgejo,
		Capabilities: CapabilityWebhook | CapabilityChecks,
	}

	if !config.Supports(CapabilityWebhook) {
		t.Fatal("expected webhook support")
	}
	if !config.Supports(CapabilityChecks) {
		t.Fatal("expected checks support")
	}
	if config.Supports(CapabilityChangeRequest) {
		t.Fatal("did not expect change request support")
	}
}

func TestChangeRequestMappingCandidatesCoverProviderDifferences(t *testing.T) {
	mappings := ChangeRequestMappingCandidates()
	if len(mappings) != 3 {
		t.Fatalf("mapping count: got %d want 3", len(mappings))
	}

	seen := make(map[ProviderID]ChangeRequestMappingCandidate, len(mappings))
	for _, mapping := range mappings {
		seen[mapping.Provider] = mapping
		assertChangeRequestMappingTargets(t, mapping)
		assertChangeRequestMappingStates(t, mapping)
		assertChangeRequestMappingActions(t, mapping)
		if len(mapping.ExtensionFields) == 0 {
			t.Fatalf("%s mapping should define provider extension fields", mapping.Provider)
		}
	}

	for _, provider := range []ProviderID{ProviderGitHub, ProviderGitLab, ProviderGitea} {
		if _, ok := seen[provider]; !ok {
			t.Fatalf("missing %s change request mapping", provider)
		}
	}
	if seen[ProviderGitHub].ProviderObject != "pull_request" {
		t.Fatalf("github provider object: %q", seen[ProviderGitHub].ProviderObject)
	}
	if seen[ProviderGitLab].ProviderObject != "merge_request" {
		t.Fatalf("gitlab provider object: %q", seen[ProviderGitLab].ProviderObject)
	}
	if seen[ProviderGitea].ProviderObject != "pull_request" {
		t.Fatalf("gitea provider object: %q", seen[ProviderGitea].ProviderObject)
	}
}

func TestChangeRequestProviderExtensionsStayOutOfCoreDTO(t *testing.T) {
	crType := reflect.TypeOf(core.ChangeRequest{})
	for _, field := range []string{"Extensions", "ExtensionFields", "ProviderFields", "Raw"} {
		if _, ok := crType.FieldByName(field); ok {
			t.Fatalf("provider-specific field %q must not live on core.ChangeRequest", field)
		}
	}
}

func assertChangeRequestMappingTargets(t *testing.T, mapping ChangeRequestMappingCandidate) {
	t.Helper()
	targets := make(map[string]bool, len(mapping.Fields))
	for _, field := range mapping.Fields {
		targets[field.Target] = true
		if field.Source == "" {
			t.Fatalf("%s field %q has empty source", mapping.Provider, field.Target)
		}
	}
	for _, target := range []string{
		"provider",
		"external_id",
		"repo_id",
		"source_branch",
		"target_branch",
		"title",
		"body",
		"draft",
		"state",
	} {
		if !targets[target] {
			t.Fatalf("%s mapping missing target field %q", mapping.Provider, target)
		}
	}
}

func assertChangeRequestMappingStates(t *testing.T, mapping ChangeRequestMappingCandidate) {
	t.Helper()
	states := make(map[core.ChangeRequestState]bool, len(mapping.States))
	for _, state := range mapping.States {
		states[state.State] = true
		if state.Source == "" {
			t.Fatalf("%s state %q has empty source", mapping.Provider, state.State)
		}
	}
	for _, state := range []core.ChangeRequestState{
		core.ChangeRequestOpen,
		core.ChangeRequestMerged,
		core.ChangeRequestClosed,
	} {
		if !states[state] {
			t.Fatalf("%s mapping missing state %q", mapping.Provider, state)
		}
	}
}

func assertChangeRequestMappingActions(t *testing.T, mapping ChangeRequestMappingCandidate) {
	t.Helper()
	actions := make(map[core.ChangeRequestAction]bool, len(mapping.Actions))
	for _, action := range mapping.Actions {
		actions[action] = true
	}
	for _, action := range []core.ChangeRequestAction{
		core.ChangeRequestActionCreate,
		core.ChangeRequestActionUpdate,
		core.ChangeRequestActionComment,
		core.ChangeRequestActionRequestReview,
		core.ChangeRequestActionMerge,
		core.ChangeRequestActionClose,
	} {
		if !actions[action] {
			t.Fatalf("%s mapping missing action %q", mapping.Provider, action)
		}
	}
}

type fakeChangeRequestAdapter struct {
	created       core.ChangeRequestCreate
	updatedID     string
	updated       core.ChangeRequestUpdate
	commentedID   string
	commented     core.ChangeRequestComment
	reviewedID    string
	reviewed      core.ChangeRequestReviewRequest
	mergedID      string
	merged        core.ChangeRequestMerge
	closedID      string
	closed        core.ChangeRequestClose
	changeRequest core.ChangeRequest
}

func (f *fakeChangeRequestAdapter) CreateChangeRequest(ctx context.Context, input core.ChangeRequestCreate) (core.ChangeRequest, error) {
	f.created = input
	return f.changeRequest, nil
}

func (f *fakeChangeRequestAdapter) UpdateChangeRequest(ctx context.Context, id string, input core.ChangeRequestUpdate) (core.ChangeRequest, error) {
	f.updatedID = id
	f.updated = input
	return f.changeRequest, nil
}

func (f *fakeChangeRequestAdapter) CommentChangeRequest(ctx context.Context, id string, input core.ChangeRequestComment) error {
	f.commentedID = id
	f.commented = input
	return nil
}

func (f *fakeChangeRequestAdapter) RequestChangeRequestReview(ctx context.Context, id string, input core.ChangeRequestReviewRequest) error {
	f.reviewedID = id
	f.reviewed = input
	return nil
}

func (f *fakeChangeRequestAdapter) MergeChangeRequest(ctx context.Context, id string, input core.ChangeRequestMerge) (core.ChangeRequest, error) {
	f.mergedID = id
	f.merged = input
	return f.changeRequest, nil
}

func (f *fakeChangeRequestAdapter) CloseChangeRequest(ctx context.Context, id string, input core.ChangeRequestClose) (core.ChangeRequest, error) {
	f.closedID = id
	f.closed = input
	return f.changeRequest, nil
}

var _ ChangeRequestAdapter = (*fakeChangeRequestAdapter)(nil)

func TestChangeRequestAdapterContractActions(t *testing.T) {
	title := "Updated title"
	draft := true
	fake := &fakeChangeRequestAdapter{
		changeRequest: core.ChangeRequest{
			Provider:     string(ProviderGitHub),
			ExternalID:   "42",
			RepoID:       "repo-1",
			SourceBranch: "feature/a",
			TargetBranch: "main",
			Title:        "Add CR model",
			Body:         "body",
			Draft:        false,
			State:        core.ChangeRequestOpen,
		},
	}

	created, err := fake.CreateChangeRequest(context.Background(), core.ChangeRequestCreate{
		RepoID:       "repo-1",
		SourceBranch: "feature/a",
		TargetBranch: "main",
		Title:        "Add CR model",
		Draft:        false,
	})
	if err != nil {
		t.Fatalf("CreateChangeRequest: %v", err)
	}
	if created.ExternalID != "42" || fake.created.Title != "Add CR model" {
		t.Fatalf("create action: created=%+v input=%+v", created, fake.created)
	}

	updated, err := fake.UpdateChangeRequest(context.Background(), "42", core.ChangeRequestUpdate{
		Title: &title,
		Draft: &draft,
	})
	if err != nil {
		t.Fatalf("UpdateChangeRequest: %v", err)
	}
	if updated.ExternalID != "42" || fake.updatedID != "42" || fake.updated.Title == nil || *fake.updated.Title != title {
		t.Fatalf("update action: updated=%+v id=%q input=%+v", updated, fake.updatedID, fake.updated)
	}
	if fake.updated.Draft == nil || *fake.updated.Draft != draft {
		t.Fatalf("update draft: %+v", fake.updated)
	}

	if err := fake.CommentChangeRequest(context.Background(), "42", core.ChangeRequestComment{Body: "looks good"}); err != nil {
		t.Fatalf("CommentChangeRequest: %v", err)
	}
	if fake.commentedID != "42" || fake.commented.Body != "looks good" {
		t.Fatalf("comment action: id=%q input=%+v", fake.commentedID, fake.commented)
	}

	if err := fake.RequestChangeRequestReview(context.Background(), "42", core.ChangeRequestReviewRequest{Reviewers: []string{"alice"}}); err != nil {
		t.Fatalf("RequestChangeRequestReview: %v", err)
	}
	if fake.reviewedID != "42" || len(fake.reviewed.Reviewers) != 1 || fake.reviewed.Reviewers[0] != "alice" {
		t.Fatalf("request review action: id=%q input=%+v", fake.reviewedID, fake.reviewed)
	}

	merged, err := fake.MergeChangeRequest(context.Background(), "42", core.ChangeRequestMerge{
		CommitTitle:   "Merge CR",
		CommitMessage: "Merge provider-neutral CR",
	})
	if err != nil {
		t.Fatalf("MergeChangeRequest: %v", err)
	}
	if merged.ExternalID != "42" || fake.mergedID != "42" || fake.merged.CommitTitle != "Merge CR" {
		t.Fatalf("merge action: merged=%+v id=%q input=%+v", merged, fake.mergedID, fake.merged)
	}

	closed, err := fake.CloseChangeRequest(context.Background(), "42", core.ChangeRequestClose{Reason: "superseded"})
	if err != nil {
		t.Fatalf("CloseChangeRequest: %v", err)
	}
	if closed.ExternalID != "42" || fake.closedID != "42" || fake.closed.Reason != "superseded" {
		t.Fatalf("close action: closed=%+v id=%q input=%+v", closed, fake.closedID, fake.closed)
	}
}

// --- Fake adapter for testing ---

// fakeWebhookAdapter is a test implementation of WebhookAdapter.
type fakeWebhookAdapter struct {
	verified     bool
	verifyErr    error
	candidates   []WebhookCandidate
	normalizeErr error

	// Recorded requests for verifier and normalizer.
	verifyReq    WebhookRequest
	normalizeReq WebhookRequest
}

func (f *fakeWebhookAdapter) VerifyWebhook(ctx context.Context, req WebhookRequest) error {
	if f.verifyErr != nil {
		return f.verifyErr
	}
	f.verified = true
	f.verifyReq = req
	return nil
}

func (f *fakeWebhookAdapter) NormalizeWebhook(ctx context.Context, req WebhookRequest) ([]WebhookCandidate, error) {
	if f.normalizeErr != nil {
		return nil, f.normalizeErr
	}
	f.normalizeReq = req
	return f.candidates, nil
}

// verifyWebhookAdapter ensures WebhookAdapter compile-time assertion.
var _ WebhookAdapter = (*fakeWebhookAdapter)(nil)

func TestWebhookAdapterContractCompileTime(t *testing.T) {
	// fakeWebhookAdapter is already assigned to var _ WebhookAdapter.
	// This test passes if the file compiles.
}

func TestWebhookAdapterContractRequestDelivery(t *testing.T) {
	revision := &core.RevisionEvent{
		RepoID:     "test-repo",
		Branch:     "main",
		Before:     "abc123",
		After:      "def456",
		ObservedAt: time.Now().UTC(),
	}
	now := time.Now().UTC()

	req := WebhookRequest{
		Provider:   ProviderForgejo,
		EventType:  "push",
		ExternalID: "delivery-123",
		Payload:    []byte(`{"ref":"refs/heads/main"}`),
		Headers: map[string][]string{
			"X-Forgejo-Signature": {"sha256=abc"},
		},
		Query: map[string][]string{
			"repo_id": {"test-repo"},
		},
		ReceivedAt: now,
	}

	fake := &fakeWebhookAdapter{
		candidates: []WebhookCandidate{
			{
				Type:       "branch.updated",
				Provider:   ProviderForgejo,
				ExternalID: "delivery-123",
				Revision:   revision,
			},
		},
	}

	ctx := context.Background()

	// Verify delivery
	if err := fake.VerifyWebhook(ctx, req); err != nil {
		t.Fatalf("VerifyWebhook: %v", err)
	}
	if !fake.verified {
		t.Fatal("expected verified to be true")
	}

	// Normalize delivery
	candidates, err := fake.NormalizeWebhook(ctx, req)
	if err != nil {
		t.Fatalf("NormalizeWebhook: %v", err)
	}
	if len(candidates) != 1 {
		t.Fatalf("expected 1 candidate, got %d", len(candidates))
	}

	c := candidates[0]
	if c.Type != "branch.updated" {
		t.Fatalf("Type: got %q want %q", c.Type, "branch.updated")
	}
	if c.Provider != ProviderForgejo {
		t.Fatalf("Provider: got %q want %q", c.Provider, ProviderForgejo)
	}
	if c.ExternalID != "delivery-123" {
		t.Fatalf("ExternalID: got %q want %q", c.ExternalID, "delivery-123")
	}
	if c.Revision == nil {
		t.Fatal("expected Revision to be set")
	}
	if c.Revision.RepoID != "test-repo" {
		t.Fatalf("Revision.RepoID: got %q want %q", c.Revision.RepoID, "test-repo")
	}
	if c.Revision.Branch != "main" {
		t.Fatalf("Revision.Branch: got %q want %q", c.Revision.Branch, "main")
	}

	// Verify that the request was actually delivered to the verifier.
	if fake.verifyReq.Provider == "" {
		t.Fatal("expected verifyReq.Provider to be set")
	}
	if fake.verifyReq.Provider != ProviderForgejo {
		t.Fatalf("verifyReq.Provider: got %q want %q", fake.verifyReq.Provider, ProviderForgejo)
	}
	if fake.verifyReq.EventType != "push" {
		t.Fatalf("verifyReq.EventType: got %q want %q", fake.verifyReq.EventType, "push")
	}
	if fake.verifyReq.ExternalID != "delivery-123" {
		t.Fatalf("verifyReq.ExternalID: got %q want %q", fake.verifyReq.ExternalID, "delivery-123")
	}
	if string(fake.verifyReq.Payload) != `{"ref":"refs/heads/main"}` {
		t.Fatalf("verifyReq.Payload: got %q", fake.verifyReq.Payload)
	}
	if len(fake.verifyReq.Headers) != 1 {
		t.Fatalf("verifyReq.Headers: got %d entries, want 1", len(fake.verifyReq.Headers))
	}
	if sigs, ok := fake.verifyReq.Headers["X-Forgejo-Signature"]; !ok {
		t.Fatal("verifyReq.Headers['X-Forgejo-Signature'] not found")
	} else if len(sigs) != 1 || sigs[0] != "sha256=abc" {
		t.Fatalf("verifyReq.Headers['X-Forgejo-Signature']: got %v, want [sha256=abc]", sigs)
	}
	if len(fake.verifyReq.Query) != 1 {
		t.Fatalf("verifyReq.Query: got %d entries, want 1", len(fake.verifyReq.Query))
	}
	if repos, ok := fake.verifyReq.Query["repo_id"]; !ok {
		t.Fatal("verifyReq.Query['repo_id'] not found")
	} else if len(repos) != 1 || repos[0] != "test-repo" {
		t.Fatalf("verifyReq.Query['repo_id']: got %v, want [test-repo]", repos)
	}
	if !fake.verifyReq.ReceivedAt.Equal(now) {
		t.Fatalf("verifyReq.ReceivedAt: got %v, want %v", fake.verifyReq.ReceivedAt, now)
	}

	// Verify that the request was actually delivered to the normalizer.
	if fake.normalizeReq.Provider != ProviderForgejo {
		t.Fatalf("normalizeReq.Provider: got %q want %q", fake.normalizeReq.Provider, ProviderForgejo)
	}
	if fake.normalizeReq.EventType != "push" {
		t.Fatalf("normalizeReq.EventType: got %q want %q", fake.normalizeReq.EventType, "push")
	}
	if fake.normalizeReq.ExternalID != "delivery-123" {
		t.Fatalf("normalizeReq.ExternalID: got %q want %q", fake.normalizeReq.ExternalID, "delivery-123")
	}
	if string(fake.normalizeReq.Payload) != `{"ref":"refs/heads/main"}` {
		t.Fatalf("normalizeReq.Payload: got %q", fake.normalizeReq.Payload)
	}
	if len(fake.normalizeReq.Headers) != 1 {
		t.Fatalf("normalizeReq.Headers: got %d entries, want 1", len(fake.normalizeReq.Headers))
	}
	if sigs, ok := fake.normalizeReq.Headers["X-Forgejo-Signature"]; !ok {
		t.Fatal("normalizeReq.Headers['X-Forgejo-Signature'] not found")
	} else if len(sigs) != 1 || sigs[0] != "sha256=abc" {
		t.Fatalf("normalizeReq.Headers['X-Forgejo-Signature']: got %v, want [sha256=abc]", sigs)
	}
	if len(fake.normalizeReq.Query) != 1 {
		t.Fatalf("normalizeReq.Query: got %d entries, want 1", len(fake.normalizeReq.Query))
	}
	if repos, ok := fake.normalizeReq.Query["repo_id"]; !ok {
		t.Fatal("normalizeReq.Query['repo_id'] not found")
	} else if len(repos) != 1 || repos[0] != "test-repo" {
		t.Fatalf("normalizeReq.Query['repo_id']: got %v, want [test-repo]", repos)
	}
	if !fake.normalizeReq.ReceivedAt.Equal(now) {
		t.Fatalf("normalizeReq.ReceivedAt: got %v, want %v", fake.normalizeReq.ReceivedAt, now)
	}
}

func TestWebhookRequestEmptyPayloadPreservation(t *testing.T) {
	req := WebhookRequest{
		Provider:   ProviderGitHub,
		EventType:  "pull_request",
		ExternalID: "",
		Payload:    []byte{},
		Headers:    nil,
		Query:      nil,
		ReceivedAt: time.Time{},
	}

	if len(req.Payload) != 0 {
		t.Fatalf("expected empty payload, got %d bytes", len(req.Payload))
	}
	if req.ExternalID != "" {
		t.Fatalf("expected empty ExternalID, got %q", req.ExternalID)
	}
	if !req.ReceivedAt.IsZero() {
		t.Fatalf("expected zero ReceivedAt")
	}
}

func TestWebhookCandidateNoSecretRequirement(t *testing.T) {
	// WebhookCandidate must not expose any secret-related fields.
	// This is verified by the struct definition itself:
	// - Type string
	// - Provider ProviderID
	// - ExternalID string
	// - Revision *core.RevisionEvent
	// No secret, signature, or token fields.
	c := WebhookCandidate{
		Type:       "branch.updated",
		Provider:   ProviderForgejo,
		ExternalID: "delivery-1",
	}

	// Verify the struct has no secret-related fields by construction.
	// If any secret field were added, compilation would still pass,
	// but the test ensures the current minimal metadata contract.
	if c.Type == "" {
		t.Fatal("Type should be settable")
	}
	if c.Provider == "" {
		t.Fatal("Provider should be settable")
	}
}
