package report_test

import (
	"bytes"
	"errors"
	"log"
	"os"
	"reflect"
	"slices"
	"testing"
	"unsafe"

	"hakurei.app/check"
	"hakurei.app/internal/kobject"
	"hakurei.app/internal/report"
	"hakurei.app/internal/stub"
	"hakurei.app/internal/uevent"
)

type representableUE struct{ stub.UniqueError }

func (representableUE) Representable() {}

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

	testCases := []struct {
		name string
		flag uint64
		errs []report.Error

		persist   bool
		wantFiles []string
		wantLog   string
		wantPanic error
	}{
		{"default", 0, []report.Error{
			{
				Severity: report.Fatal,
				Message:  "rejecting coldboot loop",
				Err:      stub.UniqueError(0xcafe),
			},
			{
				Severity: report.Inconsistent,
				Message:  "processed inconsistent uevent",
				Err: (&kobject.Event{
					Action:   uevent.KOBJ_ADD,
					DevPath:  "\x00",
					Env:      map[string]string{"V": "\xfd"},
					Sequence: 2,
				}).NewError(kobject.EDuplicateAdd, &kobject.Object{
					State:   kobject.StateNew,
					DevPath: "\x00",
					Env:     map[string]string{"V": "\xff"},
				}),
			},
		}, true, []string{`{"kind":"fatal","message":"rejecting coldboot loop","error":"unique error 51966 injected by the test suite"}
`, `{"kind":"inconsistent","message":"processed inconsistent uevent","error":{"fault":1,"event":{"action":"add","devpath":"\u0000","env":{"V":"\ufffd"},"seqnum":2,"subsystem":""},"object":{"state":1,"devpath":"\u0000","subsystem":"","env":{"V":"\ufffd"}}}}
`}, `dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite
dispatch: processed inconsistent uevent: duplicate add event on devpath "\x00"
`, nil},

		{"strict", report.DStrict, []report.Error{
			{
				Severity: report.Degraded,
				Message:  "rejecting coldboot loop",
				Err:      stub.UniqueError(0xcafe),
			},
		}, true, []string{
			`{"kind":"degradation","message":"rejecting coldboot loop","error":"unique error 51966 injected by the test suite"}
`,
		}, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", report.Error{
			Severity: report.Degraded,
			Message:  "rejecting coldboot loop",
			Err:      stub.UniqueError(0xcafe),
		}},

		{"strict no recover", report.DStrict | report.DNoRecover, []report.Error{
			{
				Severity: report.Inconsistent,
				Message:  "rejecting coldboot loop",
				Err:      stub.UniqueError(0xcafe),
			},
		}, true, []string{
			`{"kind":"inconsistent","message":"rejecting coldboot loop","error":"unique error 51966 injected by the test suite"}
`,
		}, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", report.Error{
			Severity: report.Inconsistent,
			Message:  "rejecting coldboot loop",
			Err:      stub.UniqueError(0xcafe),
		}},

		{"no recover", report.DNoRecover, []report.Error{
			{
				Severity: 0xbeef,
				Message:  "rejecting coldboot loop",
				Err:      representableUE{stub.UniqueError(0xcafe)},
			},
		}, true, []string{
			`{"kind":48879,"message":"rejecting coldboot loop","error":{"UniqueError":51966}}
`,
		}, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", nil},

		{"early", 0, []report.Error{
			{
				Severity: report.Fatal,
				Message:  "rejecting coldboot loop",
				Err:      stub.UniqueError(0xcafe),
			},
		}, false, nil, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", report.Error{
			Severity: report.Fatal,
			Message:  "rejecting coldboot loop",
			Err:      stub.UniqueError(0xcafe),
		}},

		{"bypass early", report.DBypassEarly, []report.Error{
			{
				Severity: report.Fatal,
				Message:  "rejecting coldboot loop",
				Err:      stub.UniqueError(0xcafe),
			},
		}, false, nil, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", nil},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			var r report.Reporter
			r.SetFlags(tc.flag)
			var buf bytes.Buffer
			l := log.New(&buf, "dispatch: ", 0)
			r.SetOutput(l)

			a := check.MustAbs(t.TempDir())
			if tc.persist {
				r.SetPathname(a)
			}

			var got []report.Error
			r.Notify(func(p report.Error) { got = append(got, p) })

			var gotPanic any
			func() {
				defer func() { gotPanic = recover() }()
				for _, p := range tc.errs {
					r.Dispatch(p.Severity, p.Message, p.Err)
				}
			}()

			if gotPanic == nil && !reflect.DeepEqual(got, tc.errs) {
				t.Errorf("Dispatch: %#v, want %#v", got, tc.errs)
			}
			if !reflect.DeepEqual(gotPanic, tc.wantPanic) {
				t.Errorf("Dispatch: panic = %v, want %v", gotPanic, tc.wantPanic)
			}
			if gotLog := buf.String(); gotLog != tc.wantLog {
				t.Errorf("Dispatch: log =\n%s\nwant\n%s", gotLog, tc.wantLog)
			}

			if tc.persist {
				var names []string
				if dents, err := os.ReadDir(a.String()); err != nil {
					t.Fatal(err)
				} else {
					names = make([]string, len(dents))
					for i, dent := range dents {
						names[i] = dent.Name()
					}
					slices.Sort(names)
				}

				gotFiles := make([]string, len(names))
				for i, name := range names {
					if p, err := os.ReadFile(a.Append(name).String()); err != nil {
						t.Fatal(err)
					} else {
						gotFiles[i] = unsafe.String(unsafe.SliceData(p), len(p))
					}
				}

				if !slices.Equal(gotFiles, tc.wantFiles) {
					t.Errorf("Dispatch: persist = %s, want %s", gotFiles, tc.wantFiles)
				}
			}
		})
	}
}

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

	var r report.Reporter
	r.SetFlags(report.DNoRecover)
	r.SetPathname(check.MustAbs("/proc/nonexistent"))

	var pathError *os.PathError
	func() {
		defer func() {
			if !errors.As(recover().(error), &pathError) {
				t.Fatal("invalid panic kind")
			}
		}()
		r.Dispatch(report.Fatal, "\x00", stub.UniqueError(0xbad))
	}()
	if !errors.Is(pathError.Err, os.ErrNotExist) {
		t.Fatalf("Dispatch: panic = %v", pathError)
	}

	var buf bytes.Buffer
	l := log.New(&buf, "persist: ", 0)
	r.SetOutput(l)
	r.SetFlags(0)
	r.Dispatch(report.Fatal, "\x00", stub.UniqueError(0xbad))
}
