package auth

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

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

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

// fakeTokenDB is a db.DBTX double that replays one canned statement result, so
// the single-token revocation verdict can be exercised without a live database.
// Mirrors fakeExec in internal/system/repository_application_access_test.go.
type fakeTokenDB struct {
	tag   pgconn.CommandTag
	err   error
	calls int
	sql   string
	args  []interface{}
}

func (f *fakeTokenDB) Exec(_ context.Context, sql string, args ...interface{}) (pgconn.CommandTag, error) {
	f.calls++
	f.sql = sql
	f.args = args
	return f.tag, f.err
}

func (f *fakeTokenDB) Query(context.Context, string, ...interface{}) (pgx.Rows, error) {
	return nil, nil
}
func (f *fakeTokenDB) QueryRow(context.Context, string, ...interface{}) pgx.Row { return nil }

// TestDeleteTokenPropagatesRowCount is the A17 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.DeleteToken(...)`) drops the zero-row WARN and fails the
// second case below.
func TestDeleteTokenPropagatesRowCount(t *testing.T) {
	storeErr := errors.New("token delete failed")

	tests := []struct {
		name       string
		tag        string
		execErr    error
		wantErr    error
		wantLogged bool
	}{
		{"deleted one token", "DELETE 1", nil, nil, false},
		{"zero rows is surfaced, not raised", "DELETE 0", nil, nil, true},
		{"error passes through unchanged", "DELETE 0", storeErr, storeErr, false},
		{"error with rows also passes through", "DELETE 1", storeErr, storeErr, false},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			ex := &fakeTokenDB{tag: pgconn.NewCommandTag(tc.tag), err: tc.execErr}
			r := &Repository{q: db.New(ex)}
			var buf bytes.Buffer

			gotErr := r.DeleteToken(loggedCtx(&buf), 42)

			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)
			}
			// Zero must stay a WARN, not an error: a repeat logout legitimately
			// deletes 0, and the handler turns any error into a 500.
			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, "token deletion matched zero rows") {
				t.Errorf("zero-row deletion not logged at WARN: %q", out)
			}
			if !strings.Contains(out, "level=WARN") {
				t.Errorf("zero-row deletion must log at WARN: %q", out)
			}
			// The ids must reach the log, or the signal is unreadable.
			if !strings.Contains(out, "token_id=42") || !strings.Contains(out, "deleted_tokens=0") {
				t.Errorf("log missing token_id=42 / deleted_tokens=0: %q", out)
			}
			// No credential material may appear in the log.
			for _, secret := range []string{"token", "hash", "password", "secret"} {
				if strings.Contains(strings.ToLower(out), secret+"=") {
					t.Errorf("log leaks %q: %q", secret, out)
				}
			}
		})
	}
}

// TestDeleteTokenPassesOnlyTheID pins the statement the count belongs to: the
// predicate is the bare token id, so zero means the row is gone, and nothing but
// that id is ever handed to the driver.
func TestDeleteTokenPassesOnlyTheID(t *testing.T) {
	ex := &fakeTokenDB{tag: pgconn.NewCommandTag("DELETE 1")}
	r := &Repository{q: db.New(ex)}

	if err := r.DeleteToken(loggedCtx(&bytes.Buffer{}), 7); err != nil {
		t.Fatalf("DeleteToken: %v", err)
	}
	if !strings.Contains(ex.sql, "DELETE FROM personal_access_tokens WHERE id = $1") {
		t.Errorf("unexpected statement: %q", ex.sql)
	}
	if len(ex.args) != 1 || ex.args[0] != int64(7) {
		t.Errorf("args = %#v, want [7]", ex.args)
	}
}
