/*
Copyright 2019 The Knative Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package apis

import (
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/equality"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// TestStatus is to validate ConditionAccessor interface works
type TestStatus struct {
	c Conditions
}

func (t *TestStatus) GetConditions() Conditions {
	return t.c
}

func (t *TestStatus) SetConditions(conditions Conditions) {
	t.c = conditions
}

var (
	ignoreFields = cmpopts.IgnoreFields(Condition{}, "LastTransitionTime", "Severity")
)

func TestGetCondition(t *testing.T) {
	condSet := NewLivingConditionSet()
	cases := []struct {
		name   string
		status ConditionsAccessor
		get    ConditionType
		expect *Condition
	}{{
		name: "simple",
		status: &TestStatus{c: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		}}},
		get: ConditionReady,
		expect: &Condition{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		},
	}, {
		name:   "nil",
		status: nil,
		get:    ConditionReady,
		expect: nil,
	}, {
		name: "missing",
		status: &TestStatus{c: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		}}},
		get:    "Missing",
		expect: nil,
	}}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			e, a := tc.expect, condSet.Manage(tc.status).GetCondition(tc.get)
			if diff := cmp.Diff(e, a, ignoreFields); diff != "" {
				t.Errorf("%s (-want, +got) = %v", tc.name, diff)
			}
		})
	}
}

func TestSetCondition(t *testing.T) {
	condSet := NewLivingConditionSet()
	cases := []struct {
		name   string
		status ConditionsAccessor
		set    Condition
		expect *Condition
	}{{
		name: "simple",
		status: &TestStatus{c: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionFalse,
		}}},
		set: Condition{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		},
		expect: &Condition{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		},
	}, {
		name:   "nil",
		status: nil,
		set: Condition{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		},
		expect: nil,
	}, {
		name:   "empty",
		status: &TestStatus{},
		set: Condition{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		},
		expect: &Condition{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		},
	}}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			condSet.Manage(tc.status).SetCondition(tc.set)
			e, a := tc.expect, condSet.Manage(tc.status).GetCondition(tc.set.Type)
			if diff := cmp.Diff(e, a, ignoreFields); diff != "" {
				t.Errorf("%s (-want, +got) = %v", tc.name, diff)
			}
		})
	}
}

func TestIsHappy(t *testing.T) {
	cases := []struct {
		name    string
		status  ConditionsAccessor
		condSet ConditionSet
		isHappy bool
	}{{
		name: "empty accessor should not be ready",
		status: &TestStatus{
			c: Conditions(nil),
		},
		condSet: NewLivingConditionSet(),
		isHappy: false,
	}, {
		name: "Different condition type should not be ready",
		status: &TestStatus{
			c: Conditions{{
				Type:   "Foo",
				Status: corev1.ConditionTrue,
			}},
		},
		condSet: NewLivingConditionSet(),
		isHappy: false,
	}, {
		name: "False condition accessor should not be ready",
		status: &TestStatus{
			c: Conditions{{
				Type:   ConditionReady,
				Status: corev1.ConditionFalse,
			}},
		},
		condSet: NewLivingConditionSet(),
		isHappy: false,
	}, {
		name: "Unknown condition accessor should not be ready",
		status: &TestStatus{
			c: Conditions{{
				Type:   ConditionReady,
				Status: corev1.ConditionUnknown,
			}},
		},
		condSet: NewLivingConditionSet(),
		isHappy: false,
	}, {
		name: "Missing condition accessor should not be ready",
		status: &TestStatus{
			c: Conditions{{
				Type: ConditionReady,
			}},
		},
		condSet: NewLivingConditionSet(),
		isHappy: false,
	}, {
		name: "True condition accessor should be ready",
		status: &TestStatus{
			c: Conditions{{
				Type:   ConditionReady,
				Status: corev1.ConditionTrue,
			}},
		},
		condSet: NewLivingConditionSet(),
		isHappy: true,
	}, {
		name: "Multiple conditions with ready accessor should be ready",
		status: &TestStatus{
			c: Conditions{{
				Type:   "Foo",
				Status: corev1.ConditionTrue,
			}, {
				Type:   ConditionReady,
				Status: corev1.ConditionTrue,
			}},
		},
		condSet: NewLivingConditionSet(),
		isHappy: true,
	}, {
		name: "Multiple conditions with ready accessor false should not be ready",
		status: &TestStatus{
			c: Conditions{{
				Type:   "Foo",
				Status: corev1.ConditionTrue,
			}, {
				Type:   ConditionReady,
				Status: corev1.ConditionFalse,
			}},
		},
		condSet: NewLivingConditionSet(),
		isHappy: false,
	}, {
		name: "Multiple conditions with mixed ready accessor, some don't matter, ready",
		status: &TestStatus{
			c: Conditions{{
				Type:   "Foo",
				Status: corev1.ConditionTrue,
			}, {
				Type:   "Bar",
				Status: corev1.ConditionFalse,
			}, {
				Type:   ConditionReady,
				Status: corev1.ConditionTrue,
			}},
		},
		condSet: NewLivingConditionSet(),
		isHappy: true,
	}, {
		name: "Multiple conditions with mixed ready accessor, some don't matter, not ready",
		status: &TestStatus{
			c: Conditions{{
				Type:   "Foo",
				Status: corev1.ConditionTrue,
			}, {
				Type:   "Bar",
				Status: corev1.ConditionTrue,
			}, {
				Type:   ConditionReady,
				Status: corev1.ConditionFalse,
			}},
		},
		condSet: NewLivingConditionSet(),
		isHappy: false,
	}}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if e, a := tc.isHappy, tc.condSet.Manage(tc.status).IsHappy(); e != a {
				t.Errorf("%q expected: %v got: %v", tc.name, e, a)
			}
		})
	}
}

func TestUpdateLastTransitionTime(t *testing.T) {
	condSet := NewLivingConditionSet()

	cases := []struct {
		name       string
		conditions Conditions
		condition  Condition
		update     bool
	}{{
		name: "LastTransitionTime should be set",
		conditions: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionFalse,
		}},

		condition: Condition{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		},
		update: true,
	}, {
		name: "LastTransitionTime should update",
		conditions: Conditions{{
			Type:               ConditionReady,
			Status:             corev1.ConditionFalse,
			LastTransitionTime: VolatileTime{metav1.NewTime(time.Unix(1337, 0))},
		}},
		condition: Condition{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		},
		update: true,
	}, {
		name: "if LastTransitionTime is the only chance, don't do it",
		conditions: Conditions{{
			Type:               ConditionReady,
			Status:             corev1.ConditionFalse,
			LastTransitionTime: VolatileTime{metav1.NewTime(time.Unix(1337, 0))},
		}},

		condition: Condition{
			Type:   ConditionReady,
			Status: corev1.ConditionFalse,
		},
		update: false,
	}}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {

			conds := &TestStatus{c: tc.conditions}

			was := condSet.Manage(conds).GetCondition(tc.condition.Type)
			condSet.Manage(conds).SetCondition(tc.condition)
			now := condSet.Manage(conds).GetCondition(tc.condition.Type)

			if e, a := tc.condition.Status, now.Status; e != a {
				t.Errorf("%q expected: %v to match %v", tc.name, e, a)
			}

			if tc.update {
				if e, a := was.LastTransitionTime, now.LastTransitionTime; e == a {
					t.Errorf("%q expected: %v to not match %v", tc.name, e, a)
				}
			} else {
				if e, a := was.LastTransitionTime, now.LastTransitionTime; e != a {
					t.Errorf("%q expected: %v to match %v", tc.name, e, a)
				}
			}
		})
	}
}

func TestResourceConditions(t *testing.T) {
	condSet := NewLivingConditionSet()

	status := &TestStatus{}

	foo := Condition{
		Type:   "Foo",
		Status: "True",
	}
	bar := Condition{
		Type:   "Bar",
		Status: "True",
	}

	// Add a new condition.
	condSet.Manage(status).SetCondition(foo)

	if got, want := len(status.c), 1; got != want {
		t.Fatalf("Unexpected Condition length; got %d, want %d", got, want)
	}

	// Add a second condition.
	condSet.Manage(status).SetCondition(bar)

	if got, want := len(status.c), 2; got != want {
		t.Fatalf("Unexpected Condition length; got %d, want %d", got, want)
	}
}

func TestConditionSeverity(t *testing.T) {
	condSet := NewLivingConditionSet("Foo")
	status := &TestStatus{}

	// Add a new condition.
	condSet.Manage(status).InitializeConditions()

	if got, want := len(status.c), 2; got != want {
		t.Errorf("Unexpected number of conditions: %d, wanted %d", got, want)
	}

	condSet.Manage(status).MarkFalse("Bar", "", "")

	if got, want := len(status.c), 3; got != want {
		t.Errorf("Unexpected number of conditions: %d, wanted %d", got, want)
	}

	if got, want := condSet.Manage(status).GetCondition("Ready").Severity, ConditionSeverityError; got != want {
		t.Errorf("GetCondition(%q).Severity = %v, wanted %v", "Ready", got, want)
	}

	if got, want := condSet.Manage(status).GetCondition("Foo").Severity, ConditionSeverityError; got != want {
		t.Errorf("GetCondition(%q).Severity = %v, wanted %v", "Foo", got, want)
	}

	if got, want := condSet.Manage(status).GetCondition("Bar").Severity, ConditionSeverityInfo; got != want {
		t.Errorf("GetCondition(%q).Severity = %v, wanted %v", "Bar", got, want)
	}
}

// getTypes is a small helped to strip out the used ConditionTypes from Conditions
func getTypes(conds Conditions) []ConditionType {
	types := make([]ConditionType, 0, len(conds))
	for _, c := range conds {
		types = append(types, c.Type)
	}
	return types
}

type ConditionMarkTrueTest struct {
	name           string
	conditions     Conditions
	conditionTypes []ConditionType
	mark           ConditionType
	happy          bool
	happyWant      *Condition
}

func doTestMarkTrueAccessor(t *testing.T, cases []ConditionMarkTrueTest) {
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			conditionTypes := tc.conditionTypes
			if conditionTypes == nil {
				conditionTypes = getTypes(tc.conditions)
			}
			condSet := NewLivingConditionSet(conditionTypes...)
			status := &TestStatus{c: tc.conditions}
			condSet.Manage(status).InitializeConditions()

			condSet.Manage(status).MarkTrue(tc.mark)

			if e, a := tc.happy, condSet.Manage(status).IsHappy(); e != a {
				t.Errorf("%q expected: %v got: %v", tc.name, e, a)
			} else if !e && tc.happyWant != nil {
				e, a := tc.happyWant, condSet.Manage(status).GetTopLevelCondition()
				if diff := cmp.Diff(e, a, ignoreFields); diff != "" {
					t.Errorf("%s (-want, +got) = %v", tc.name, diff)
				}
			}

			if tc.mark == condSet.happy {
				// Skip validation the happy condition because we can't be sure
				// marking it true was correct. Use tc.happyWant to test that case.
				return
			}

			expected := &Condition{
				Type:   tc.mark,
				Status: corev1.ConditionTrue,
			}

			e, a := expected, condSet.Manage(status).GetCondition(tc.mark)
			if diff := cmp.Diff(e, a, ignoreFields); diff != "" {
				t.Errorf("%s (-want, +got) = %v", tc.name, diff)
			}
		})
		// Run same test with MarkTrueWithReason
		t.Run(tc.name+" with reason", func(t *testing.T) {
			conditionTypes := tc.conditionTypes
			if conditionTypes == nil {
				conditionTypes = getTypes(tc.conditions)
			}
			condSet := NewLivingConditionSet(conditionTypes...)
			status := &TestStatus{c: tc.conditions}
			condSet.Manage(status).InitializeConditions()

			condSet.Manage(status).MarkTrueWithReason(tc.mark, "UnitTest", "calm down, just testing")

			if e, a := tc.happy, condSet.Manage(status).IsHappy(); e != a {
				t.Errorf("%q expected: %v got: %v", tc.name, e, a)
			} else if !e && tc.happyWant != nil {
				e, a := tc.happyWant, condSet.Manage(status).GetTopLevelCondition()
				if diff := cmp.Diff(e, a, ignoreFields); diff != "" {
					t.Errorf("%s (-want, +got) = %v", tc.name, diff)
				}
			}

			if tc.mark == condSet.happy {
				// Skip validation the happy condition because we can't be sure
				// marking it true was correct. Use tc.happyWant to test that case.
				return
			}

			expected := &Condition{
				Type:    tc.mark,
				Status:  corev1.ConditionTrue,
				Reason:  "UnitTest",
				Message: "calm down, just testing",
			}

			e, a := expected, condSet.Manage(status).GetCondition(tc.mark)
			if diff := cmp.Diff(e, a, ignoreFields); diff != "" {
				t.Errorf("%s (-want, +got) = %v", tc.name, diff)
			}
		})
	}
}

func TestMarkTrue(t *testing.T) {
	cases := []ConditionMarkTrueTest{{
		name:  "no deps",
		mark:  ConditionReady,
		happy: true,
	}, {
		name: "existing conditions, turns happy",
		conditions: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionFalse,
		}},
		mark:  ConditionReady,
		happy: true,
	}, {
		name: "with deps, happy",
		conditions: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionFalse,
		}, {
			Type:   "Foo",
			Status: corev1.ConditionTrue,
		}},
		mark:  ConditionReady,
		happy: true,
	}, {
		name: "with deps, not happy",
		conditions: Conditions{{
			Type:    ConditionReady,
			Status:  corev1.ConditionFalse,
			Reason:  "ReadyReason",
			Message: "ReadyMsg",
		}, {
			Type:    "Foo",
			Status:  corev1.ConditionFalse,
			Reason:  "FooReason",
			Message: "FooMsg",
		}},
		mark:  ConditionReady,
		happy: false,
		happyWant: &Condition{
			Type:    ConditionReady,
			Status:  corev1.ConditionFalse,
			Reason:  "FooReason",
			Message: "FooMsg",
		},
	}, {
		name: "update dep, turns happy",
		conditions: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionFalse,
		}, {
			Type:   "Foo",
			Status: corev1.ConditionFalse,
		}},
		mark:  "Foo",
		happy: true,
	}, {
		name: "update dep, happy was unknown, turns happy",
		conditions: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionUnknown,
		}, {
			Type:   "Foo",
			Status: corev1.ConditionFalse,
		}},
		mark:  "Foo",
		happy: true,
	}, {
		name: "update dep 1/2, still not happy",
		conditions: Conditions{{
			Type:    ConditionReady,
			Status:  corev1.ConditionFalse,
			Reason:  "FooReason",
			Message: "FooMsg",
		}, {
			Type:    "Foo",
			Status:  corev1.ConditionFalse,
			Reason:  "FooReason",
			Message: "FooMsg",
		}, {
			Type:    "Bar",
			Status:  corev1.ConditionFalse,
			Reason:  "BarReason",
			Message: "BarMsg",
		}},
		mark:  "Foo",
		happy: false,
		happyWant: &Condition{
			Type:    ConditionReady,
			Status:  corev1.ConditionFalse,
			Reason:  "BarReason",
			Message: "BarMsg",
		},
	}, {
		name: "update dep 1/3, mixed status, still not happy",
		conditions: Conditions{{
			Type:    ConditionReady,
			Status:  corev1.ConditionFalse,
			Reason:  "FooReason",
			Message: "FooMsg",
		}, {
			Type:    "Foo",
			Status:  corev1.ConditionFalse,
			Reason:  "FooReason",
			Message: "FooMsg",
		}, {
			Type:    "Bar",
			Status:  corev1.ConditionUnknown,
			Reason:  "BarReason",
			Message: "BarMsg",
		}, {
			Type:    "Baz",
			Status:  corev1.ConditionFalse,
			Reason:  "BazReason",
			Message: "BazMsg",
		}},
		mark:  "Foo",
		happy: false,
		happyWant: &Condition{
			Type:    ConditionReady,
			Status:  corev1.ConditionFalse,
			Reason:  "BazReason",
			Message: "BazMsg",
		},
	}, {
		name: "update dep 1/3, unknown status, still not happy",
		conditions: Conditions{{
			Type:    ConditionReady,
			Status:  corev1.ConditionFalse,
			Reason:  "FooReason",
			Message: "FooMsg",
		}, {
			Type:    "Foo",
			Status:  corev1.ConditionFalse,
			Reason:  "FooReason",
			Message: "FooMsg",
		}, {
			Type:    "Bar",
			Status:  corev1.ConditionUnknown,
			Reason:  "BarReason",
			Message: "BarMsg",
		}, {
			Type:    "Baz",
			Status:  corev1.ConditionUnknown,
			Reason:  "BazReason",
			Message: "BazMsg",
		}},
		mark:  "Foo",
		happy: false,
		happyWant: &Condition{
			Type:    ConditionReady,
			Status:  corev1.ConditionUnknown,
			Reason:  "BarReason",
			Message: "BarMsg",
		},
	}, {
		name: "update dep 1/3, unknown status because nil",
		conditions: Conditions{{
			Type:    ConditionReady,
			Status:  corev1.ConditionFalse,
			Reason:  "FooReason",
			Message: "FooMsg",
		}, {
			Type:    "Foo",
			Status:  corev1.ConditionFalse,
			Reason:  "FooReason",
			Message: "FooMsg",
		}},
		mark:           "Foo",
		conditionTypes: []ConditionType{"Foo", "Bar", "Baz"},
		happy:          false,
		happyWant: &Condition{
			Type:   ConditionReady,
			Status: corev1.ConditionUnknown,
		},
	}, {
		name: "update deps all happy with extra cruft",
		conditions: Conditions{{
			Type:    ConditionReady,
			Status:  corev1.ConditionFalse,
			Reason:  "FooReason",
			Message: "FooMsg",
		}, {
			Type:    "Foo",
			Status:  corev1.ConditionFalse,
			Reason:  "FooReason",
			Message: "FooMsg",
		}, {
			Type:     "Bar",
			Status:   corev1.ConditionUnknown,
			Reason:   "BarReason",
			Message:  "BarMsg",
			Severity: "FYI",
		}, {
			Type:     "Baz",
			Status:   corev1.ConditionUnknown,
			Reason:   "BazReason",
			Message:  "BazMsg",
			Severity: "LOLJK",
		}},
		mark:           "Foo",
		conditionTypes: []ConditionType{"Foo"},
		happy:          true,
		happyWant: &Condition{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		},
	}, {
		name: "with no dependents with extra cruft",
		conditions: Conditions{{
			Type:    ConditionReady,
			Status:  corev1.ConditionFalse,
			Reason:  "LongStory",
			Message: "Set manually",
		}, {
			Type:    "Foo",
			Status:  corev1.ConditionFalse,
			Reason:  "FooReason",
			Message: "FooMsg",
		}, {
			Type:     "Bar",
			Status:   corev1.ConditionUnknown,
			Reason:   "BarReason",
			Message:  "BarMsg",
			Severity: "FYI",
		}, {
			Type:     "Baz",
			Status:   corev1.ConditionUnknown,
			Reason:   "BazReason",
			Message:  "BazMsg",
			Severity: "LOLJK",
		}},
		mark:           "Foo",
		conditionTypes: []ConditionType{},
		happy:          true,
		happyWant: &Condition{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		},
	}, {
		name: "happy dependents with extra cruft",
		conditions: Conditions{{
			Type:    ConditionReady,
			Status:  corev1.ConditionFalse,
			Reason:  "LongStory",
			Message: "Set manually",
		}, {
			Type:   "Foo",
			Status: corev1.ConditionTrue,
		}, {
			Type:     "Bar",
			Status:   corev1.ConditionUnknown,
			Reason:   "BarReason",
			Message:  "BarMsg",
			Severity: "FYI",
		}, {
			Type:     "Baz",
			Status:   corev1.ConditionUnknown,
			Reason:   "BazReason",
			Message:  "BazMsg",
			Severity: "LOLJK",
		}},
		mark:           "Bar",
		conditionTypes: []ConditionType{"Foo"},
		happy:          true,
		happyWant: &Condition{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		},
	}, {
		name: "all happy but not cover all dependents",
		conditions: Conditions{{
			Type:    ConditionReady,
			Status:  corev1.ConditionFalse,
			Reason:  "LongStory",
			Message: "Set manually",
		}, {
			Type:   "Foo",
			Status: corev1.ConditionTrue,
		}},
		mark:           "Foo",
		conditionTypes: []ConditionType{"Foo", "Bar"}, // dependents is more than conditions.
		happy:          false,
		happyWant: &Condition{
			Type:   ConditionReady,
			Status: corev1.ConditionUnknown,
		},
	}, {
		name: "all happy and cover all dependents",
		conditions: Conditions{{
			Type:    ConditionReady,
			Status:  corev1.ConditionFalse,
			Reason:  "LongStory",
			Message: "Set manually",
		}, {
			Type:   "Foo",
			Status: corev1.ConditionTrue,
		}, {
			Type:   "NewCondition",
			Status: corev1.ConditionTrue,
		}},
		mark:           "Foo",
		conditionTypes: []ConditionType{"Foo"}, // dependents is less than conditions.
		happy:          true,
		happyWant: &Condition{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		},
	}}
	doTestMarkTrueAccessor(t, cases)
}

type ConditionMarkFalseTest struct {
	name       string
	conditions Conditions
	mark       ConditionType
	unhappy    bool
}

func doTestMarkFalseAccessor(t *testing.T, cases []ConditionMarkFalseTest) {
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {

			condSet := NewLivingConditionSet(getTypes(tc.conditions)...)
			status := &TestStatus{c: tc.conditions}
			condSet.Manage(status).InitializeConditions()

			condSet.Manage(status).MarkFalse(tc.mark, "UnitTest", "calm down, just testing")

			if e, a := !tc.unhappy, condSet.Manage(status).IsHappy(); e != a {
				t.Errorf("%q expected: %v got: %v", tc.name, e, a)
			}

			expected := &Condition{
				Type:    tc.mark,
				Status:  corev1.ConditionFalse,
				Reason:  "UnitTest",
				Message: "calm down, just testing",
			}

			e, a := expected, condSet.Manage(status).GetCondition(tc.mark)
			if diff := cmp.Diff(e, a, ignoreFields); diff != "" {
				t.Errorf("%s (-want, +got) = %v", tc.name, diff)
			}
		})
	}
}

func TestMarkFalse(t *testing.T) {
	cases := []ConditionMarkFalseTest{{
		name:    "no deps",
		mark:    ConditionReady,
		unhappy: true,
	}, {
		name: "existing conditions, turns unhappy",
		conditions: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		}},
		mark:    ConditionReady,
		unhappy: true,
	}, {
		name: "with deps, turns unhappy",
		conditions: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		}, {
			Type:   "Foo",
			Status: corev1.ConditionTrue,
		}},
		mark:    ConditionReady,
		unhappy: true,
	}, {
		name: "with deps, turns unhappy",
		conditions: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		}, {
			Type:   "Foo",
			Status: corev1.ConditionFalse,
		}},
		mark:    ConditionReady,
		unhappy: true,
	}, {
		name: "update dep, turns unhappy",
		conditions: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		}, {
			Type:   "Foo",
			Status: corev1.ConditionTrue,
		}},
		mark:    "Foo",
		unhappy: true,
	}, {
		name: "update dep, happy was unknown, turns unhappy",
		conditions: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionUnknown,
		}, {
			Type:   "Foo",
			Status: corev1.ConditionFalse,
		}},
		mark:    "Foo",
		unhappy: true,
	}, {
		name: "update dep 1/2, turns unhappy",
		conditions: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		}, {
			Type:   "Foo",
			Status: corev1.ConditionTrue,
		}, {
			Type:   "Bar",
			Status: corev1.ConditionTrue,
		}},
		mark:    "Foo",
		unhappy: true,
	}}
	doTestMarkFalseAccessor(t, cases)
}

type ConditionMarkUnknownTest struct {
	name       string
	conditions Conditions
	mark       ConditionType
	unhappy    bool
	happyIs    corev1.ConditionStatus
}

func doTestMarkUnknownAccessor(t *testing.T, cases []ConditionMarkUnknownTest) {
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {

			condSet := NewLivingConditionSet(getTypes(tc.conditions)...)
			status := &TestStatus{c: tc.conditions}

			condSet.Manage(status).MarkUnknown(tc.mark, "UnitTest", "idk, just testing")

			if e, a := !tc.unhappy, condSet.Manage(status).IsHappy(); e != a {
				t.Errorf("%q expected IsHappy: %v got: %v", tc.name, e, a)
			}

			if e, a := tc.happyIs, condSet.Manage(status).GetCondition(ConditionReady).Status; e != a {
				t.Errorf("%q expected ConditionReady: %v got: %v", tc.name, e, a)
			}

			expected := &Condition{
				Type:    tc.mark,
				Status:  corev1.ConditionUnknown,
				Reason:  "UnitTest",
				Message: "idk, just testing",
			}

			e, a := expected, condSet.Manage(status).GetCondition(tc.mark)
			if diff := cmp.Diff(e, a, ignoreFields); diff != "" {
				t.Errorf("%s (-want, +got) = %v", tc.name, diff)
			}
		})
	}
}

func TestMarkUnknown(t *testing.T) {
	cases := []ConditionMarkUnknownTest{{
		name:    "no deps",
		mark:    ConditionReady,
		unhappy: true,
		happyIs: corev1.ConditionUnknown,
	}, {
		name: "existing conditions, turns unhappy",
		conditions: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		}},
		mark:    ConditionReady,
		unhappy: true,
		happyIs: corev1.ConditionUnknown,
	}, {
		name: "with deps, turns unhappy",
		conditions: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		}, {
			Type:   "Foo",
			Status: corev1.ConditionTrue,
		}},
		mark:    ConditionReady,
		unhappy: true,
		happyIs: corev1.ConditionUnknown,
	}, {
		name: "with deps that are false, turns unhappy",
		conditions: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		}, {
			Type:   "Foo",
			Status: corev1.ConditionFalse,
		}, {
			Type:   "Bar",
			Status: corev1.ConditionFalse,
		}},
		mark:    "Foo",
		unhappy: true,
		happyIs: corev1.ConditionFalse,
	}, {
		name: "update dep, turns unhappy",
		conditions: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		}, {
			Type:   "Foo",
			Status: corev1.ConditionTrue,
		}},
		mark:    "Foo",
		unhappy: true,
		happyIs: corev1.ConditionUnknown,
	}, {
		name: "update dep, happy was unknown, turns unhappy",
		conditions: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionUnknown,
		}, {
			Type:   "Foo",
			Status: corev1.ConditionFalse,
		}},
		mark:    "Foo",
		unhappy: true,
		happyIs: corev1.ConditionUnknown,
	}, {
		name: "update dep 1/2, turns unhappy",
		conditions: Conditions{{
			Type:   ConditionReady,
			Status: corev1.ConditionTrue,
		}, {
			Type:   "Foo",
			Status: corev1.ConditionTrue,
		}, {
			Type:   "Bar",
			Status: corev1.ConditionTrue,
		}},
		mark:    "Foo",
		unhappy: true,
		happyIs: corev1.ConditionUnknown,
	}}
	doTestMarkUnknownAccessor(t, cases)
}

func TestInitializeConditions(t *testing.T) {
	condSet := NewLivingConditionSet()

	cases := []struct {
		name       string
		conditions Conditions
		want       *Condition
	}{{
		name: "no conditions is initialized",
		want: &Condition{
			Type:     ConditionReady,
			Status:   corev1.ConditionUnknown,
			Severity: ConditionSeverityError,
		},
	}, {
		name: "initialization is idempotent",
		conditions: Conditions{{
			Type:     ConditionReady,
			Status:   corev1.ConditionUnknown,
			Severity: ConditionSeverityError,
		}},
		want: &Condition{
			Type:     ConditionReady,
			Status:   corev1.ConditionUnknown,
			Severity: ConditionSeverityError,
		},
		// TODO(#357): Uncomment once fixed.
		// }, {
		// 	name: "initialization overwrites existing",
		// 	conditions: Conditions{{
		// 		Type:     ConditionReady,
		// 		Status:   corev1.ConditionFalse,
		// 		Severity: ConditionSeverityError,
		// 	}},
		// 	want: &Condition{
		// 		Type:     ConditionReady,
		// 		Status:   corev1.ConditionUnknown,
		// 		Severity: ConditionSeverityError,
		// 	},
	}}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			status := &TestStatus{c: tc.conditions}
			condSet.Manage(status).InitializeConditions()
			if e, a := tc.want, condSet.Manage(status).GetCondition(ConditionReady); !equality.Semantic.DeepEqual(e, a) {
				t.Errorf("accessor, %q expected: %v got: %v", tc.name, e, a)
			}
		})
	}
}

func TestTerminalInitialization(t *testing.T) {
	set := NewLivingConditionSet("Foo")
	status := &TestStatus{}

	manager := set.Manage(status)
	manager.InitializeConditions()

	if got, want := len(status.c), 2; got != want {
		t.Errorf("InitializeConditions() = %v, wanted %v", got, want)
	}

	manager.MarkTrue("Foo")
	if !manager.IsHappy() {
		t.Error("IsHappy() = false, wanted true")
	}

	// Add a new condition "Bar" to simulate the addition of conditions.
	set = NewLivingConditionSet("Foo", "Bar")

	// Create a new manager for the new set and re-initialize to simulate
	// Reconcile() with the new conditions.
	manager = set.Manage(status)
	manager.InitializeConditions()

	if got, want := len(status.c), 3; got != want {
		t.Errorf("InitializeConditions() = %v, wanted %v", got, want)
	}

	if c := manager.GetCondition("Bar"); c == nil {
		t.Error("GetCondition(Bar) = nil, wanted True")
	} else if got, want := c.Status, corev1.ConditionTrue; got != want {
		t.Errorf("GetCondition(Bar) = %s, wanted %s", got, want)
	}
}

func TestRemoveNonTerminalConditions(t *testing.T) {
	set := NewLivingConditionSet("Foo")
	status := &TestStatus{}

	manager := set.Manage(status)
	manager.MarkTrue("Foo")
	manager.MarkTrue("Bar")

	if got, want := len(status.c), 3; got != want {
		t.Errorf("Marking true() = %v, wanted %v", got, want)
	}

	if !manager.IsHappy() {
		t.Error("IsHappy() = false, wanted true")
	}

	err := manager.ClearCondition("Bar")
	if err != nil {
		t.Error("Clear condition should not return err", err)
	}
	if got, want := len(status.c), 2; got != want {
		t.Errorf("Marking true() = %v, wanted %v", got, want)
	}

	if !manager.IsHappy() {
		t.Error("IsHappy() = false, wanted true")
	}
}

func TestClearConditionWithNilManager(t *testing.T) {
	set := NewLivingConditionSet("Foo")
	manager := set.Manage(nil)

	err := manager.ClearCondition("Bar")
	if err != nil {
		t.Error("ClearCondition() expected to return nil if status is nil, got", err)
	}

}
