package jira

import (
	"encoding/json"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/alertstore/pkg/alert"
	"github.com/tidbcloud/alertstore/pkg/instance"
)

// buildConfig is a prod deployment: the project and summary tag jiralert's prod
// receivers used.
func buildConfig() Config {
	return Config{Project: "O11Y", SummaryPrefix: "[PROD]"}
}

func testInstance(t *testing.T, state string, labels, annotations map[string]string) instance.Instance {
	t.Helper()
	labelsJSON, err := json.Marshal(labels)
	require.NoError(t, err)
	annotationsJSON, err := json.Marshal(annotations)
	require.NoError(t, err)
	return instance.Instance{
		Fingerprint: "abcdef0123456789aaaa",
		State:       state,
		Labels:      labelsJSON,
		Annotations: annotationsJSON,
		StartsAt:    time.Date(2026, 7, 30, 10, 0, 0, 0, time.UTC),
	}
}

// A compatibility contract: jiralert wrote "[PROD]<alertname>" and automation
// matches on it.
func TestBuildIssueMirrorsJiralertsShape(t *testing.T) {
	t.Parallel()

	attrs := &alert.AlertAttrs{
		Alertname:      "TiKVDiskAlmostFull",
		Classification: alert.ClassPage,
		ClusterID:      "1379661944646493359",
		ClusterName:    "production",
		TenantID:       "1372813089206751289",
		Component:      "tikv",
		Namespace:      "tidb1379661944646493359",
		BizType:        "dedicated",
		Region:         "us-west-2",
		Summary:        "TiKV disk is almost full",
		ClinicURL:      "https://clinic.pingcap.com/portal/#/clusters?query=1379661944646493359",
		Labels:         map[string]string{"severity": "critical"},
	}
	a := &alert.Alert{ID: "ALT-2607-42"}

	issue := BuildIssue(buildConfig(), a, attrs, nil, "https://alerts.ops-dev.pingcap.net/ui/#/alerts/ALT-2607-42")

	require.Equal(t, "[PROD]TiKVDiskAlmostFull", issue.Summary)
	require.Equal(t, "O11Y", issue.Project)
	require.Equal(t, "Task", issue.IssueType)
	require.Equal(t, "Critical", issue.Priority)
	require.Equal(t, []string{"tikv"}, issue.Components)
	require.Equal(t, "1372813089206751289", issue.CustomFields["customfield_10157"])
	require.Equal(t, "1379661944646493359", issue.CustomFields["customfield_10149"])

	// The alert ID is the only link back from Jira to the alertstore record.
	require.Contains(t, issue.Labels, "ALT-2607-42")
	require.Contains(t, issue.Labels, "alertstore")

	require.Contains(t, issue.Description, "[ALT-2607-42|https://alerts.ops-dev.pingcap.net/ui/#/alerts/ALT-2607-42]")
	require.Contains(t, issue.Description, "| *Cluster ID:* 1379661944646493359 |")
	require.Contains(t, issue.Description, "| *Tenant ID:* 1372813089206751289 |")
	require.Contains(t, issue.Description, "| *Biz Type:* dedicated |")
	require.Contains(t, issue.Description, "TiKV disk is almost full")
}

// The tenant's commercial profile: the description prints both fields, and the
// kind also goes into the custom field jiraport's assignment rules read to tell
// an internal alert from a customer's — the same one jiralert wrote.
func TestBuildIssueCarriesTheTenantProfile(t *testing.T) {
	t.Parallel()

	attrs := &alert.AlertAttrs{
		Alertname:  "TiKVDiskAlmostFull",
		TenantID:   "1372813089206751289",
		TenantName: "ACME Corp",
		TenantKind: "internal",
		BizRegion:  "NA",
		Region:     "us-west-2",
	}

	issue := BuildIssue(buildConfig(), &alert.Alert{ID: "ALT-2607-42"}, attrs, nil, "")

	require.Equal(t, "internal", issue.CustomFields["customfield_10159"])
	require.Contains(t, issue.Description, "| *Tenant Name:* ACME Corp |")
	require.Contains(t, issue.Description, "| *Tenant Kind:* internal |")
	require.Contains(t, issue.Description, "| *Biz Region:* NA |")
	// The cloud region stays its own row: where it runs is not whose it is.
	require.Contains(t, issue.Description, "| *Region:* us-west-2 |")
}

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

	require.Equal(t, "Major", priorityFor(&alert.AlertAttrs{Classification: alert.ClassTicket}))
	require.Equal(t, "Critical", priorityFor(&alert.AlertAttrs{Classification: alert.ClassPage}))
	// severity wins where set: jiralert's per-receiver priorities came from it.
	require.Equal(t, "Warning", priorityFor(&alert.AlertAttrs{
		Classification: alert.ClassPage,
		Labels:         map[string]string{"severity": "warning"},
	}))
}

// Jira rejects a component the project does not define, costing the whole issue.
func TestUnknownComponentBecomesOther(t *testing.T) {
	t.Parallel()

	require.Equal(t, "tikv", mapComponent("tikv"))
	require.Equal(t, "other", mapComponent("some-service-nobody-registered"))
	require.Equal(t, "other", mapComponent(""))
}

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

	inst := testInstance(t, instance.StateFiring,
		map[string]string{"persistentvolumeclaim": "data-tikv-0", "severity": "critical"},
		map[string]string{"summary": "disk at 87%"})

	issue := BuildIssue(buildConfig(), &alert.Alert{ID: "ALT-2607-42"},
		&alert.AlertAttrs{Alertname: "TiKVDiskAlmostFull"}, []instance.Instance{inst}, "")

	require.Contains(t, issue.Description, "|| *Alert Details* ||")
	require.Contains(t, issue.Description, "persistentvolumeclaim: data-tikv-0")
	require.Contains(t, issue.Description, "summary: disk at 87%")
}

// An aggregate lists its members instead; alertstore holds the detail.
func TestAggregateListsOccurrencesAndBoundsTheTable(t *testing.T) {
	t.Parallel()

	instances := []instance.Instance{
		testInstance(t, instance.StateResolved, map[string]string{"persistentvolumeclaim": "data-tikv-0"}, nil),
		testInstance(t, instance.StateFiring, map[string]string{"persistentvolumeclaim": "data-tikv-1"}, nil),
	}
	issue := BuildIssue(buildConfig(), &alert.Alert{ID: "ALT-2607-42"},
		&alert.AlertAttrs{Alertname: "TiKVDiskAlmostFull"}, instances, "")

	require.Contains(t, issue.Description, "|| *Instances* || 1 firing / 2 total ||")
	// Firing sorts above recovered.
	require.Less(t,
		strings.Index(issue.Description, "data-tikv-1"),
		strings.Index(issue.Description, "data-tikv-0"))

	many := make([]instance.Instance, maxListedInstances+5)
	for i := range many {
		many[i] = testInstance(t, instance.StateFiring, map[string]string{"pod": "tikv-" + string(rune('a'+i%26))}, nil)
	}
	bounded := BuildIssue(buildConfig(), &alert.Alert{ID: "ALT-2607-42"},
		&alert.AlertAttrs{Alertname: "TiKVDiskAlmostFull"}, many, "")
	require.Contains(t, bounded.Description, "and 5 more — see alertstore")
}

// The header describes the whole issue, so a field the occurrences disagree on
// may not be printed as if they agreed — alertmanager draws the same line
// between commonLabels and per-alert labels.
func TestHeaderNamesDivergentFieldsInsteadOfTheOpeningOccurrence(t *testing.T) {
	t.Parallel()

	// region is outside alert.AggLabels, so occurrences that disagree on it still
	// fold into one alert.
	instances := []instance.Instance{
		testInstance(t, instance.StateFiring,
			map[string]string{"component": "tikv", "region": "us-east-1", "tidb_cluster_id": "tidb42"}, nil),
		testInstance(t, instance.StateFiring,
			map[string]string{"component": "tikv", "region": "us-west-2", "tidb_cluster_id": "tidb42"}, nil),
	}
	attrs := &alert.AlertAttrs{
		Alertname: "TiKVDiskAlmostFull", ClusterID: "42",
		ClusterName: "production", Component: "tikv", Region: "us-east-1",
	}
	issue := BuildIssue(buildConfig(), &alert.Alert{ID: "ALT-2607-42"}, attrs, instances, "")

	require.Contains(t, issue.Description, "| *Region:* us-east-1, us-west-2 |")
	// What they do agree on reads exactly as before.
	require.Contains(t, issue.Description, "| *Cluster ID:* 42 |")
	// Clinic enrichment has no occurrence-level counterpart and survives.
	require.Contains(t, issue.Description, "| *Cluster Name:* production |")
	// The Jira component field stays single-valued for automation.
	require.Equal(t, []string{mapComponent("tikv")}, issue.Components)
}

// Automation parses this field, so it carries the opening occurrence.
func TestRawAlertFieldCarriesTheOpeningOccurrence(t *testing.T) {
	t.Parallel()

	inst := testInstance(t, instance.StateFiring,
		map[string]string{"alertname": "TiKVDiskAlmostFull"}, map[string]string{"summary": "disk at 87%"})
	issue := BuildIssue(buildConfig(), &alert.Alert{ID: "ALT-2607-42"},
		&alert.AlertAttrs{Alertname: "TiKVDiskAlmostFull"}, []instance.Instance{inst}, "")

	var raw map[string]interface{}
	require.NoError(t, json.Unmarshal([]byte(issue.CustomFields["customfield_10160"].(string)), &raw))
	require.Equal(t, "firing", raw["status"])
	require.Equal(t, "TiKVDiskAlmostFull", raw["labels"].(map[string]interface{})["alertname"])
}

// Jira rejects some fields set to "", so an alert with no tenant or cluster omits
// them rather than sending blanks.
func TestEmptyCustomFieldsAreOmitted(t *testing.T) {
	t.Parallel()

	issue := BuildIssue(buildConfig(), &alert.Alert{ID: "ALT-2607-42"},
		&alert.AlertAttrs{Alertname: "TiKVDiskAlmostFull"}, nil, "")

	require.NotContains(t, issue.CustomFields, "customfield_10157")
	require.NotContains(t, issue.CustomFields, "customfield_10149")
	require.NotContains(t, issue.CustomFields, "customfield_10159")
	// The raw-alert field always goes: automation parses it.
	require.Contains(t, issue.CustomFields, "customfield_10160")
}

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

	issue := BuildIssue(buildConfig(), &alert.Alert{ID: "ALT-2607-42"},
		&alert.AlertAttrs{Alertname: "TiKV Disk Almost Full", Classification: alert.ClassPage}, nil, "")

	for _, label := range issue.Labels {
		require.NotContains(t, label, " ", "Jira rejects labels containing whitespace")
	}
	require.Contains(t, issue.Labels, "TiKV-Disk-Almost-Full")
}
