package catalogsystem

import (
	"encoding/json"
	"errors"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	systemauthorityv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/systemlayoutbundleauthorityv1"
)

func TestWriterBindingRejectsInvalidOrAmbiguousAuthority(t *testing.T) {
	db, _, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	authority, err := New(db)
	if err != nil {
		t.Fatal(err)
	}
	valid := writerGrant{BundleKey: testBundleKey("1"), AllowCreate: true}
	tests := map[string]struct {
		principal authenticatedPrincipal
		grants    []writerGrant
	}{
		"blank principal": {
			principal: authenticatedPrincipal{AuditDigest: testDigest("a")},
			grants:    []writerGrant{valid},
		},
		"bad audit digest": {
			principal: authenticatedPrincipal{PrincipalID: "writer", AuditDigest: "bad"},
			grants:    []writerGrant{valid},
		},
		"no grants": {
			principal: testPrincipal,
		},
		"empty operation set": {
			principal: testPrincipal,
			grants:    []writerGrant{{BundleKey: testBundleKey("1")}},
		},
		"duplicate key": {
			principal: testPrincipal,
			grants:    []writerGrant{valid, valid},
		},
	}
	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			if _, err := authority.bindAuthenticatedWriter(
				test.principal,
				test.grants,
			); err == nil {
				t.Fatal("invalid writer authority was accepted")
			}
		})
	}
}

func TestWriterGrantStorageIsDefensiveAndNonSerializable(t *testing.T) {
	db, _, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	authority, err := New(db)
	if err != nil {
		t.Fatal(err)
	}
	grants := []writerGrant{{
		BundleKey:   testBundleKey("1"),
		AllowCreate: true,
	}}
	writer, err := authority.bindAuthenticatedWriter(testPrincipal, grants)
	if err != nil {
		t.Fatal(err)
	}
	grants[0].AllowCreate = false
	if err := writer.authorize(testCreateRequest("1")); err != nil {
		t.Fatalf("input mutation changed writer authority: %v", err)
	}
	if _, err := json.Marshal(writer); err == nil {
		t.Fatal("writer capability acquired a JSON form")
	}
}

func TestOutcomeDigestCommitsEveryField(t *testing.T) {
	request := testCreateRequest("1")
	command := durableCommand{
		CommandID:     testCommand1,
		Principal:     testPrincipal,
		Request:       request,
		RequestDigest: mustRequestDigest(t, request),
	}
	base, err := completeOutcome(command, ResultApplied)
	if err != nil {
		t.Fatal(err)
	}
	if err := validateDurableOutcome(base, command); err != nil {
		t.Fatal(err)
	}
	mutations := map[string]func(*durableOutcome){
		"command": func(value *durableOutcome) {
			value.CommandID = testCommand2
		},
		"request": func(value *durableOutcome) {
			value.RequestDigest = testDigest("b")
		},
		"principal": func(value *durableOutcome) {
			value.Principal.PrincipalID = "other-writer"
		},
		"audit": func(value *durableOutcome) {
			value.Principal.AuditDigest = testDigest("c")
		},
		"result": func(value *durableOutcome) {
			value.Result = ResultExpectedPriorMismatch
		},
	}
	for name, mutate := range mutations {
		t.Run(name, func(t *testing.T) {
			changed := base
			mutate(&changed)
			changed.OutcomeDigest = ""
			digest, err := outcomeDigest(changed)
			if err != nil {
				t.Fatal(err)
			}
			if digest == base.OutcomeDigest {
				t.Fatal("field mutation retained the outcome digest")
			}
		})
	}
}

func TestDurableOutcomeRejectsBindingOrDigestDrift(t *testing.T) {
	request := testCreateRequest("1")
	command := durableCommand{
		CommandID:     testCommand1,
		Principal:     testPrincipal,
		Request:       request,
		RequestDigest: mustRequestDigest(t, request),
	}
	outcome, err := completeOutcome(command, ResultApplied)
	if err != nil {
		t.Fatal(err)
	}
	outcome.OutcomeDigest = testDigest("f")
	if err := validateDurableOutcome(outcome, command); !errors.Is(err, ErrCorrupt) {
		t.Fatalf("digest drift error = %v, want ErrCorrupt", err)
	}
	outcome, err = completeOutcome(command, ResultApplied)
	if err != nil {
		t.Fatal(err)
	}
	other := command
	other.Principal.PrincipalID = "other-writer"
	if err := validateDurableOutcome(outcome, other); !errors.Is(err, ErrCorrupt) {
		t.Fatalf("binding drift error = %v, want ErrCorrupt", err)
	}
}

func TestAuthorizeHasClosedOperationSet(t *testing.T) {
	db, _, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	authority, err := New(db)
	if err != nil {
		t.Fatal(err)
	}
	writer, err := authority.bindAuthenticatedWriter(
		testPrincipal,
		[]writerGrant{{
			BundleKey:   testBundleKey("1"),
			AllowCreate: true,
		}},
	)
	if err != nil {
		t.Fatal(err)
	}
	request := testCreateRequest("1")
	request.Operation = systemauthorityv1.Operation("UPSERT")
	if err := writer.authorize(request); !errors.Is(err, ErrUnauthorized) {
		t.Fatalf("authorize unknown operation = %v, want ErrUnauthorized", err)
	}
}
