package healthcheck_test

import (
	"context"
	"database/sql"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	gomock "go.uber.org/mock/gomock"

	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/dbmock"
	"github.com/coder/coder/v2/coderd/database/dbtime"
	"github.com/coder/coder/v2/coderd/healthcheck"
	"github.com/coder/coder/v2/coderd/healthcheck/health"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/codersdk/healthsdk"
	"github.com/coder/coder/v2/provisionerd/proto"
)

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

	now := dbtime.Now()

	for _, tt := range []struct {
		name                   string
		currentVersion         string
		currentAPIMajorVersion int
		provisionerDaemons     []database.ProvisionerDaemon
		provisionerDaemonsErr  error
		expectedSeverity       health.Severity
		expectedWarningCode    health.Code
		expectedError          string
		expectedItems          []healthsdk.ProvisionerDaemonsReportItem
	}{
		{
			name:             "current version empty",
			currentVersion:   "",
			expectedSeverity: health.SeverityError,
			expectedError:    "Developer error: CurrentVersion is empty",
			expectedItems:    []healthsdk.ProvisionerDaemonsReportItem{},
		},
		{
			name:                   "no daemons",
			currentVersion:         "v1.2.3",
			currentAPIMajorVersion: proto.CurrentMajor,
			expectedSeverity:       health.SeverityError,
			expectedItems:          []healthsdk.ProvisionerDaemonsReportItem{},
			expectedWarningCode:    health.CodeProvisionerDaemonsNoProvisionerDaemons,
		},
		{
			name:                   "error fetching daemons",
			currentVersion:         "v1.2.3",
			currentAPIMajorVersion: proto.CurrentMajor,
			provisionerDaemonsErr:  assert.AnError,
			expectedSeverity:       health.SeverityError,
			expectedError:          assert.AnError.Error(),
			expectedItems:          []healthsdk.ProvisionerDaemonsReportItem{},
		},
		{
			name:                   "one daemon up to date",
			currentVersion:         "v1.2.3",
			currentAPIMajorVersion: proto.CurrentMajor,
			expectedSeverity:       health.SeverityOK,
			provisionerDaemons:     []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0", now)},
			expectedItems: []healthsdk.ProvisionerDaemonsReportItem{
				{
					ProvisionerDaemon: codersdk.ProvisionerDaemon{
						ID:           uuid.Nil,
						Name:         "pd-ok",
						CreatedAt:    now,
						LastSeenAt:   codersdk.NewNullTime(now, true),
						Version:      "v1.2.3",
						APIVersion:   "1.0",
						Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform},
						Tags:         map[string]string{},
					},
					Warnings: []health.Message{},
				},
			},
		},
		{
			name:                   "one daemon out of date",
			currentVersion:         "v1.2.3",
			currentAPIMajorVersion: proto.CurrentMajor,
			expectedSeverity:       health.SeverityWarning,
			expectedWarningCode:    health.CodeProvisionerDaemonVersionMismatch,
			provisionerDaemons:     []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-old", "v1.1.2", "1.0", now)},
			expectedItems: []healthsdk.ProvisionerDaemonsReportItem{
				{
					ProvisionerDaemon: codersdk.ProvisionerDaemon{
						ID:           uuid.Nil,
						Name:         "pd-old",
						CreatedAt:    now,
						LastSeenAt:   codersdk.NewNullTime(now, true),
						Version:      "v1.1.2",
						APIVersion:   "1.0",
						Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform},
						Tags:         map[string]string{},
					},
					Warnings: []health.Message{
						{
							Code:    health.CodeProvisionerDaemonVersionMismatch,
							Message: `Mismatched version "v1.1.2"`,
						},
					},
				},
			},
		},
		{
			name:                   "invalid daemon version",
			currentVersion:         "v1.2.3",
			currentAPIMajorVersion: proto.CurrentMajor,
			expectedSeverity:       health.SeverityError,
			expectedWarningCode:    health.CodeUnknown,
			provisionerDaemons:     []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-invalid-version", "invalid", "1.0", now)},
			expectedItems: []healthsdk.ProvisionerDaemonsReportItem{
				{
					ProvisionerDaemon: codersdk.ProvisionerDaemon{
						ID:           uuid.Nil,
						Name:         "pd-invalid-version",
						CreatedAt:    now,
						LastSeenAt:   codersdk.NewNullTime(now, true),
						Version:      "invalid",
						APIVersion:   "1.0",
						Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform},
						Tags:         map[string]string{},
					},
					Warnings: []health.Message{
						{
							Code:    health.CodeUnknown,
							Message: `Invalid version "invalid"`,
						},
					},
				},
			},
		},
		{
			name:                   "invalid daemon api version",
			currentVersion:         "v1.2.3",
			currentAPIMajorVersion: proto.CurrentMajor,
			expectedSeverity:       health.SeverityError,
			expectedWarningCode:    health.CodeUnknown,
			provisionerDaemons:     []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-invalid-api", "v1.2.3", "invalid", now)},
			expectedItems: []healthsdk.ProvisionerDaemonsReportItem{
				{
					ProvisionerDaemon: codersdk.ProvisionerDaemon{
						ID:           uuid.Nil,
						Name:         "pd-invalid-api",
						CreatedAt:    now,
						LastSeenAt:   codersdk.NewNullTime(now, true),
						Version:      "v1.2.3",
						APIVersion:   "invalid",
						Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform},
						Tags:         map[string]string{},
					},
					Warnings: []health.Message{
						{
							Code:    health.CodeUnknown,
							Message: `Invalid API version: invalid version string: invalid`,
						},
					},
				},
			},
		},
		{
			name:                   "api version backward compat",
			currentVersion:         "v2.3.4",
			currentAPIMajorVersion: 2,
			expectedSeverity:       health.SeverityWarning,
			expectedWarningCode:    health.CodeProvisionerDaemonAPIMajorVersionDeprecated,
			provisionerDaemons:     []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-old-api", "v2.3.4", "1.0", now)},
			expectedItems: []healthsdk.ProvisionerDaemonsReportItem{
				{
					ProvisionerDaemon: codersdk.ProvisionerDaemon{
						ID:           uuid.Nil,
						Name:         "pd-old-api",
						CreatedAt:    now,
						LastSeenAt:   codersdk.NewNullTime(now, true),
						Version:      "v2.3.4",
						APIVersion:   "1.0",
						Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform},
						Tags:         map[string]string{},
					},
					Warnings: []health.Message{
						{
							Code:    health.CodeProvisionerDaemonAPIMajorVersionDeprecated,
							Message: "Deprecated major API version 1.",
						},
					},
				},
			},
		},
		{
			name:                   "one up to date, one out of date",
			currentVersion:         "v1.2.3",
			currentAPIMajorVersion: proto.CurrentMajor,
			expectedSeverity:       health.SeverityWarning,
			expectedWarningCode:    health.CodeProvisionerDaemonVersionMismatch,
			provisionerDaemons:     []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0", now), fakeProvisionerDaemon(t, "pd-old", "v1.1.2", "1.0", now)},
			expectedItems: []healthsdk.ProvisionerDaemonsReportItem{
				{
					ProvisionerDaemon: codersdk.ProvisionerDaemon{
						ID:           uuid.Nil,
						Name:         "pd-ok",
						CreatedAt:    now,
						LastSeenAt:   codersdk.NewNullTime(now, true),
						Version:      "v1.2.3",
						APIVersion:   "1.0",
						Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform},
						Tags:         map[string]string{},
					},
					Warnings: []health.Message{},
				},
				{
					ProvisionerDaemon: codersdk.ProvisionerDaemon{
						ID:           uuid.Nil,
						Name:         "pd-old",
						CreatedAt:    now,
						LastSeenAt:   codersdk.NewNullTime(now, true),
						Version:      "v1.1.2",
						APIVersion:   "1.0",
						Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform},
						Tags:         map[string]string{},
					},
					Warnings: []health.Message{
						{
							Code:    health.CodeProvisionerDaemonVersionMismatch,
							Message: `Mismatched version "v1.1.2"`,
						},
					},
				},
			},
		},
		{
			name:                   "one up to date, one newer",
			currentVersion:         "v1.2.3",
			currentAPIMajorVersion: proto.CurrentMajor,
			expectedSeverity:       health.SeverityWarning,
			expectedWarningCode:    health.CodeProvisionerDaemonVersionMismatch,
			provisionerDaemons:     []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0", now), fakeProvisionerDaemon(t, "pd-new", "v2.3.4", "1.0", now)},
			expectedItems: []healthsdk.ProvisionerDaemonsReportItem{
				{
					ProvisionerDaemon: codersdk.ProvisionerDaemon{
						ID:           uuid.Nil,
						Name:         "pd-new",
						CreatedAt:    now,
						LastSeenAt:   codersdk.NewNullTime(now, true),
						Version:      "v2.3.4",
						APIVersion:   "1.0",
						Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform},
						Tags:         map[string]string{},
					},
					Warnings: []health.Message{
						{
							Code:    health.CodeProvisionerDaemonVersionMismatch,
							Message: `Mismatched version "v2.3.4"`,
						},
					},
				},
				{
					ProvisionerDaemon: codersdk.ProvisionerDaemon{
						ID:           uuid.Nil,
						Name:         "pd-ok",
						CreatedAt:    now,
						LastSeenAt:   codersdk.NewNullTime(now, true),
						Version:      "v1.2.3",
						APIVersion:   "1.0",
						Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform},
						Tags:         map[string]string{},
					},
					Warnings: []health.Message{},
				},
			},
		},
		{
			name:                   "one up to date, one stale older",
			currentVersion:         "v2.3.4",
			currentAPIMajorVersion: proto.CurrentMajor,
			expectedSeverity:       health.SeverityOK,
			provisionerDaemons:     []database.ProvisionerDaemon{fakeProvisionerDaemonStale(t, "pd-stale", "v1.2.3", "0.9", now.Add(-5*time.Minute), now), fakeProvisionerDaemon(t, "pd-ok", "v2.3.4", "1.0", now)},
			expectedItems: []healthsdk.ProvisionerDaemonsReportItem{
				{
					ProvisionerDaemon: codersdk.ProvisionerDaemon{
						ID:           uuid.Nil,
						Name:         "pd-ok",
						CreatedAt:    now,
						LastSeenAt:   codersdk.NewNullTime(now, true),
						Version:      "v2.3.4",
						APIVersion:   "1.0",
						Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform},
						Tags:         map[string]string{},
					},
					Warnings: []health.Message{},
				},
			},
		},
		{
			name:                   "one stale",
			currentVersion:         "v2.3.4",
			currentAPIMajorVersion: proto.CurrentMajor,
			expectedSeverity:       health.SeverityError,
			expectedWarningCode:    health.CodeProvisionerDaemonsNoProvisionerDaemons,
			provisionerDaemons:     []database.ProvisionerDaemon{fakeProvisionerDaemonStale(t, "pd-ok", "v1.2.3", "0.9", now.Add(-5*time.Minute), now)},
			expectedItems:          []healthsdk.ProvisionerDaemonsReportItem{},
		},
	} {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			var rpt healthcheck.ProvisionerDaemonsReport
			var deps healthcheck.ProvisionerDaemonsReportDeps
			deps.CurrentVersion = tt.currentVersion
			deps.CurrentAPIMajorVersion = tt.currentAPIMajorVersion
			if tt.currentAPIMajorVersion == 0 {
				deps.CurrentAPIMajorVersion = proto.CurrentMajor
			}
			deps.TimeNow = func() time.Time {
				return now
			}

			ctrl := gomock.NewController(t)
			mDB := dbmock.NewMockStore(ctrl)
			mDB.EXPECT().GetProvisionerDaemons(gomock.Any()).AnyTimes().Return(tt.provisionerDaemons, tt.provisionerDaemonsErr)
			deps.Store = mDB

			rpt.Run(context.Background(), &deps)

			assert.Equal(t, tt.expectedSeverity, rpt.Severity)
			if tt.expectedWarningCode != "" && assert.NotEmpty(t, rpt.Warnings) {
				var found bool
				for _, w := range rpt.Warnings {
					if w.Code == tt.expectedWarningCode {
						found = true
						break
					}
				}
				assert.True(t, found, "expected warning %s not found in %v", tt.expectedWarningCode, rpt.Warnings)
			} else {
				assert.Empty(t, rpt.Warnings)
			}
			if tt.expectedError != "" && assert.NotNil(t, rpt.Error) {
				assert.Contains(t, *rpt.Error, tt.expectedError)
			}
			if tt.expectedItems != nil {
				assert.Equal(t, tt.expectedItems, rpt.Items)
			}
		})
	}
}

func fakeProvisionerDaemon(t *testing.T, name, version, apiVersion string, now time.Time) database.ProvisionerDaemon {
	t.Helper()
	return database.ProvisionerDaemon{
		ID:           uuid.Nil,
		Name:         name,
		CreatedAt:    now,
		LastSeenAt:   sql.NullTime{Time: now, Valid: true},
		Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho, database.ProvisionerTypeTerraform},
		ReplicaID:    uuid.NullUUID{},
		Tags:         map[string]string{},
		Version:      version,
		APIVersion:   apiVersion,
	}
}

func fakeProvisionerDaemonStale(t *testing.T, name, version, apiVersion string, lastSeenAt, now time.Time) database.ProvisionerDaemon {
	t.Helper()
	d := fakeProvisionerDaemon(t, name, version, apiVersion, now)
	d.LastSeenAt.Valid = true
	d.LastSeenAt.Time = lastSeenAt
	return d
}
