package system

import (
	"bytes"
	"errors"
	"strings"
	"testing"

	"github.com/jackc/pgx/v5/pgconn"

	"github.com/TaruLab/keluargamart-backend/internal/auth"
	"github.com/TaruLab/keluargamart-backend/internal/db"
)

// TestSetUserActivePropagatesRowCount is the A16 regression net. It drives the
// real Repository wrapper over a db.DBTX double rather than the verdict helper
// alone, so the protection actually under test is the call site: the query used
// to be :exec, which left nothing to observe. Reintroducing the discarded count
// (`_, err := r.q.SetUserActive(...)`) makes the zero-row case report a nil
// error again and fails the second case below.
func TestSetUserActivePropagatesRowCount(t *testing.T) {
	driverErr := errors.New("pq: deadlock detected")

	tests := []struct {
		name       string
		active     bool
		tag        string
		execErr    error
		wantErr    error
		wantLogged string
	}{
		{"one row deactivated", false, "UPDATE 1", nil, nil, ""},
		{"one row reactivated", true, "UPDATE 1", nil, nil, ""},
		{"zero rows is a failure, not a success", false, "UPDATE 0", nil, auth.ErrNotFound, "is_active write matched zero rows"},
		{"driver error passes through", false, "UPDATE 0", driverErr, driverErr, ""},
		{"driver error outranks the row count", true, "UPDATE 1", driverErr, driverErr, ""},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			ex := &fakeExec{tag: pgconn.NewCommandTag(tc.tag), err: tc.execErr}
			r := &Repository{q: db.New(ex)}
			var buf bytes.Buffer

			gotErr := r.SetUserActive(loggedCtx(&buf), 42, tc.active)

			if !errors.Is(gotErr, tc.wantErr) {
				t.Errorf("err = %v, want %v", gotErr, tc.wantErr)
			}
			if tc.wantErr != nil && gotErr != tc.wantErr { //nolint:errorlint // identity, not classification
				t.Errorf("error not returned unchanged: got %v, want the same value %v", gotErr, tc.wantErr)
			}
			// A deactivation is security-critical: exactly one statement, no retry
			// loop, so a count of 0 can never be papered over by a second attempt.
			if ex.calls != 1 {
				t.Errorf("Exec calls = %d, want 1", ex.calls)
			}

			out := buf.String()
			if tc.wantLogged == "" {
				if out != "" {
					t.Errorf("unexpected log output: %q", out)
				}
				return
			}
			if !strings.Contains(out, tc.wantLogged) {
				t.Errorf("zero-row write not logged: want %q, got %q", tc.wantLogged, out)
			}
			// The id must reach the log, or the signal is unactionable.
			if !strings.Contains(out, "user_id=42") {
				t.Errorf("log missing user_id=42: %q", out)
			}
			for _, secret := range []string{"password", "hash", "secret", "token"} {
				if strings.Contains(strings.ToLower(out), secret+"=") {
					t.Errorf("log leaks %q: %q", secret, out)
				}
			}
		})
	}
}

// TestSetUserActiveWritesTheColumnAndArgs pins the statement the count belongs
// to: `WHERE id = $1` with is_active passed second. If the predicate ever grows a
// business filter, the "a zero means the row is gone" reasoning behind
// auth.ErrNotFound no longer holds and this test says so.
func TestSetUserActiveWritesTheColumnAndArgs(t *testing.T) {
	ex := &fakeExec{tag: pgconn.NewCommandTag("UPDATE 1")}
	r := &Repository{q: db.New(ex)}

	if err := r.SetUserActive(loggedCtx(&bytes.Buffer{}), 7, true); err != nil {
		t.Fatalf("SetUserActive: %v", err)
	}
	if !strings.Contains(ex.sql, "UPDATE users SET is_active = $2, updated_at = now() WHERE id = $1") {
		t.Errorf("unexpected statement: %q", ex.sql)
	}
	// $1 = user id, $2 = is_active, in that order.
	if len(ex.args) != 2 || ex.args[0] != int64(7) || ex.args[1] != true {
		t.Errorf("args = %#v, want [7 true]", ex.args)
	}
}
