package packs

import (
	"context"
	"encoding/json"
	"regexp"
	"strings"
	"testing"

	sqlmock "github.com/DATA-DOG/go-sqlmock"
)

func TestSourceBackedEmailTemplateInstallableWithoutWorkflowAlert(t *testing.T) {
	components := `[
    {
      "kind": "email_template",
      "businessKey": "Opportunity_Approval_Notice",
      "manageability": "locked",
      "payload": {
        "name": "Opportunity Approval Notice",
        "developerName": "Opportunity_Approval_Notice",
        "subject": "Review {{Record.Name}}",
        "htmlValue": "<p>{{User.Email}}</p>",
        "body": "Open {{Variables.ApprovalUrl}}",
        "templateType": "custom",
        "isActive": true
      }
    }
  ]`
	root := writePackFixture(t, "jira-like-sample", validPackManifest(components, ""))

	report := ValidateInstallablePackSourceWithOptions("local_path:"+root, testPackSourceOptions(t))
	if report.HasErrors() {
		t.Fatalf("ValidateInstallablePackSourceWithOptions returned errors:\n%s", report.Error())
	}
	if report.Pack == nil {
		t.Fatal("ValidateInstallablePackSourceWithOptions returned nil pack")
	}
	declared, err := report.Pack.DeclaredComponents()
	if err != nil {
		t.Fatalf("DeclaredComponents: %v", err)
	}
	if len(declared) != 1 {
		t.Fatalf("DeclaredComponents() = %d, want 1", len(declared))
	}
	got := declared[0]
	if got.Kind != ComponentKindEmailTemplate {
		t.Fatalf("declared kind = %q, want %q", got.Kind, ComponentKindEmailTemplate)
	}
	if got.BusinessKey != "Opportunity_Approval_Notice" {
		t.Fatalf("declared businessKey = %q, want developerName", got.BusinessKey)
	}
	if got.Manageability != ManageabilityLocked {
		t.Fatalf("declared manageability = %q, want locked", got.Manageability)
	}
}

func TestEmailTemplateInstallPlanPhasePrecedesFlow(t *testing.T) {
	emailTemplateContract, ok := ComponentKindContractFor(ComponentKindEmailTemplate)
	if !ok {
		t.Fatal("email_template contract missing")
	}
	flowContract, ok := ComponentKindContractFor(ComponentKindStandardFlow)
	if !ok {
		t.Fatal("standard_flow contract missing")
	}
	emailRank, ok := InstallPlanPhaseRank(emailTemplateContract.Phase)
	if !ok {
		t.Fatalf("email_template phase %q missing from phase order", emailTemplateContract.Phase)
	}
	flowRank, ok := InstallPlanPhaseRank(flowContract.Phase)
	if !ok {
		t.Fatalf("standard_flow phase %q missing from phase order", flowContract.Phase)
	}
	if emailRank >= flowRank {
		t.Fatalf("email_template phase rank = %d, want before standard_flow=%d", emailRank, flowRank)
	}
}

func TestWorkflowAlertRemainsUnsupportedSourceBackedComponent(t *testing.T) {
	components := `[{
    "kind": "workflow_alert",
    "businessKey": "Legacy_Alert",
    "manageability": "locked",
    "payload": {"name": "Legacy Alert"}
  }]`
	root := writePackFixture(t, "jira-like-sample", validPackManifest(components, ""))

	report := ValidateInstallablePackSourceWithOptions("local_path:"+root, testPackSourceOptions(t))
	if !report.HasErrors() {
		t.Fatal("ValidateInstallablePackSourceWithOptions accepted workflow_alert; want guardrail rejection")
	}
	if !strings.Contains(report.Error(), `component kind "workflow_alert" is not source-backed`) {
		t.Fatalf("workflow_alert diagnostic = %q, want unsupported source-backed component", report.Error())
	}
}

func TestSourceBackedEmailTemplateProjectionRejectsRuntimeInvalidSyntax(t *testing.T) {
	tests := []struct {
		name      string
		subject   string
		wantError string
	}{
		{
			name:      "salesforce merge syntax",
			subject:   "Review {!Record.Name}",
			wantError: "Salesforce merge syntax is not supported",
		},
		{
			name:      "nested field path runtime cannot resolve",
			subject:   "Review {{Record.Owner.Name}}",
			wantError: "exactly one approved root and field name",
		},
		{
			name:      "segment whitespace runtime cannot resolve",
			subject:   "Review {{Record. Name}}",
			wantError: "leading or trailing whitespace",
		},
		{
			name:      "unsupported root",
			subject:   "Review {{WorkflowAlert.Name}}",
			wantError: "unsupported root",
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			payload := []byte(`{
        "developerName": "Opportunity_Approval_Notice",
        "subject": ` + quoteJSONString(tc.subject) + `,
        "body": "Open {{Variables.ApprovalUrl}}"
      }`)
			_, _, err := sourceBackedEmailTemplateProjectionFromPayload("crm-pack", ManifestComponent{
				Kind:        ComponentKindEmailTemplate,
				BusinessKey: "Opportunity_Approval_Notice",
			}, payload)
			if err == nil || !strings.Contains(err.Error(), tc.wantError) {
				t.Fatalf("projection error = %v, want %q", err, tc.wantError)
			}
		})
	}
}

func TestSourceBackedEmailTemplateBusinessKeyMustMatchDeveloperName(t *testing.T) {
	payload := []byte(`{
    "developerName": "Different_Template",
    "subject": "Review {{Record.Name}}",
    "body": "Open {{Variables.ApprovalUrl}}"
  }`)

	_, _, err := sourceBackedEmailTemplateProjectionFromPayload("crm-pack", ManifestComponent{
		Kind:        ComponentKindEmailTemplate,
		BusinessKey: "Opportunity_Approval_Notice",
	}, payload)
	if err == nil || !strings.Contains(err.Error(), `businessKey "Opportunity_Approval_Notice" must match developerName "Different_Template"`) {
		t.Fatalf("projection error = %v, want businessKey/developerName mismatch", err)
	}
}

func TestSourceBackedEmailTemplateApplyWritesOwnerPackAndDeterministicID(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	component, err := newSourceBackedEmailTemplateComponent("crm-pack", ManifestComponent{
		Kind:          ComponentKindEmailTemplate,
		BusinessKey:   "Opportunity_Approval_Notice",
		Manageability: ManageabilityLocked,
	}, []byte(`{
    "name": "Opportunity Approval Notice",
    "developerName": "Opportunity_Approval_Notice",
    "subject": " Review {{Record.Name}} ",
    "htmlValue": " <p>{{User.Email}}</p> ",
    "body": " Open {{Variables.ApprovalUrl}} ",
    "templateType": "custom"
  }`))
	if err != nil {
		t.Fatalf("newSourceBackedEmailTemplateComponent: %v", err)
	}

	wantID := sourceBackedEmailTemplateID("crm-pack", "Opportunity_Approval_Notice")
	mock.ExpectExec(regexp.QuoteMeta("INSERT INTO _System_EmailTemplate")).
		WithArgs(
			wantID,
			"Opportunity Approval Notice",
			" Review {{Record.Name}} ",
			" <p>{{User.Email}}</p> ",
			" Open {{Variables.ApprovalUrl}} ",
			nil,
			nil,
			"custom",
			"Opportunity_Approval_Notice",
			"crm-pack",
			true,
		).
		WillReturnResult(sqlmock.NewResult(1, 1))

	if err := component.Apply(context.Background(), ApplyDeps{DB: db}); err != nil {
		t.Fatalf("Apply: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestSourceBackedEmailTemplateVerifyDetectsProjectionDrift(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	component, err := newSourceBackedEmailTemplateComponent("crm-pack", ManifestComponent{
		Kind:          ComponentKindEmailTemplate,
		BusinessKey:   "Opportunity_Approval_Notice",
		Manageability: ManageabilityLocked,
	}, []byte(`{
    "name": "Opportunity Approval Notice",
    "developerName": "Opportunity_Approval_Notice",
    "subject": "Review {{Record.Name}}",
    "body": "Open {{Variables.ApprovalUrl}}"
  }`))
	if err != nil {
		t.Fatalf("newSourceBackedEmailTemplateComponent: %v", err)
	}

	mock.ExpectQuery(`SELECT Id, COALESCE\(OwnerPack, ''\), Name, DeveloperName, Subject`).
		WithArgs("crm-pack", "Opportunity_Approval_Notice").
		WillReturnRows(sqlmock.NewRows([]string{
			"Id", "OwnerPack", "Name", "DeveloperName", "Subject", "HtmlValue", "Body", "FolderId", "Description", "TemplateType", "IsActive",
		}).AddRow(
			sourceBackedEmailTemplateID("crm-pack", "Opportunity_Approval_Notice"),
			"crm-pack",
			"Opportunity Approval Notice",
			"Opportunity_Approval_Notice",
			"Changed subject",
			"",
			"Open {{Variables.ApprovalUrl}}",
			nil,
			nil,
			"custom",
			true,
		))

	err = verifyPackComponentDefault(context.Background(), VerifyDeps{DB: db}, component)
	if err == nil || !strings.Contains(err.Error(), "projection drift") {
		t.Fatalf("verifyPackComponentDefault error = %v, want projection drift", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestEmailTemplatePackSweeperSoftDeletesOrphans(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	batch := &emailTemplatePackBatch{declaredDeveloperNames: []string{"Kept_Template"}}
	mock.ExpectQuery(`SELECT Id, DeveloperName\s+FROM _System_EmailTemplate`).
		WithArgs("crm-pack").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "DeveloperName"}).
			AddRow("tmpl-orphan", "Dropped_Template").
			AddRow("tmpl-kept", "Kept_Template"))
	mock.ExpectExec(`UPDATE _System_EmailTemplate`).
		WithArgs("tmpl-orphan").
		WillReturnResult(sqlmock.NewResult(1, 1))

	if err := applyEmailTemplatePackSweeper(context.Background(), db, batch, "crm-pack"); err != nil {
		t.Fatalf("applyEmailTemplatePackSweeper: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestSourceBackedEmailTemplateDuplicateDeveloperNamesRejectedAcrossComponents(t *testing.T) {
	components := `[
    {
      "kind": "email_template",
      "businessKey": "Shared_Notice",
      "manageability": "locked",
      "payload": {
        "developerName": "Shared_Notice",
        "subject": "Review {{Record.Name}}",
        "body": "Open {{Variables.ApprovalUrl}}"
      }
    },
    {
      "kind": "email_template",
      "businessKey": "shared_notice",
      "manageability": "locked",
      "payload": {
        "developerName": "shared_notice",
        "subject": "Review {{Record.Name}}",
        "body": "Open {{Variables.ApprovalUrl}}"
      }
    }
  ]`
	root := writePackFixture(t, "jira-like-sample", validPackManifest(components, ""))

	report := ValidateInstallablePackSourceWithOptions("local_path:"+root, testPackSourceOptions(t))
	if !report.HasErrors() {
		t.Fatal("ValidateInstallablePackSourceWithOptions accepted duplicate email_template developerName")
	}
	if !strings.Contains(report.Error(), "declared more than once") {
		t.Fatalf("duplicate developerName diagnostic = %q, want duplicate developerName rejection", report.Error())
	}
}

func quoteJSONString(value string) string {
	encoded, err := json.Marshal(value)
	if err != nil {
		panic(err)
	}
	return string(encoded)
}
