package system

import (
	"bytes"
	"context"
	"errors"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/go-chi/chi/v5"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgconn"

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

// loggedCtx returns a context carrying a logger that writes to the buffer, the
// same slog-over-bytes.Buffer idiom as
// internal/auth/repository_token_revoke_test.go.
func loggedCtx(buf *bytes.Buffer) context.Context {
	return logger.ToContext(context.Background(),
		slog.New(slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelWarn})))
}

// fakeExec is a db.DBTX double that replays one canned statement result, so the
// application_access write verdict can be exercised without a live database.
type fakeExec struct {
	tag   pgconn.CommandTag
	err   error
	calls int
	sql   string
	args  []interface{}
}

func (f *fakeExec) 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 *fakeExec) Query(context.Context, string, ...interface{}) (pgx.Rows, error) { return nil, nil }
func (f *fakeExec) QueryRow(context.Context, string, ...interface{}) pgx.Row        { return nil }

// TestObserveApplicationAccess covers the decision the discarded `_, _ =` used
// to hide: a failed application_access write must be reported, never absorbed.
// There is no live database in this suite, so this exercises the verdict
// directly rather than a real UPDATE.
func TestObserveApplicationAccess(t *testing.T) {
	driverErr := errors.New(`pq: permission denied for table users (SQLSTATE 42501)`)

	tests := []struct {
		name       string
		rows       int64
		err        error
		wantErr    error
		wantLogged string
	}{
		{"one row written", 1, nil, nil, ""},
		{"zero rows is a failure, not a success", 0, nil, auth.ErrNotFound, "application_access write matched zero rows"},
		{"driver error is reported", 0, driverErr, ErrApplicationAccess, "application_access write failed"},
		{"driver error outranks the row count", 1, driverErr, ErrApplicationAccess, "application_access write failed"},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			var buf bytes.Buffer
			gotErr := observeApplicationAccess(loggedCtx(&buf), 42, tc.rows, tc.err)

			if !errors.Is(gotErr, tc.wantErr) {
				t.Errorf("err = %v, want %v", gotErr, tc.wantErr)
			}

			out := buf.String()
			if tc.wantLogged == "" {
				if out != "" {
					t.Errorf("unexpected log output: %q", out)
				}
				return
			}
			if !strings.Contains(out, tc.wantLogged) {
				t.Errorf("failure not logged: want %q, got %q", tc.wantLogged, out)
			}
			if !strings.Contains(out, "level=ERROR") {
				t.Errorf("failed privilege write must log at ERROR: %q", 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)
			}
			// respondErr echoes err.Error() to the client, so the sentinel must
			// not carry the driver/SQL text with it.
			if gotErr != nil && strings.Contains(gotErr.Error(), "SQLSTATE") {
				t.Errorf("driver detail leaked to the caller: %v", gotErr)
			}
		})
	}
}

// TestSetApplicationAccessWritesTheColumn is the happy path of the same write:
// it must target users.application_access for the right id, once, and swallow
// nothing.
func TestSetApplicationAccessWritesTheColumn(t *testing.T) {
	ex := &fakeExec{tag: pgconn.NewCommandTag("UPDATE 1")}
	if err := setApplicationAccess(loggedCtx(&bytes.Buffer{}), ex, 7, auth.AppPOS); err != nil {
		t.Fatalf("setApplicationAccess: %v", err)
	}
	if ex.calls != 1 {
		t.Errorf("Exec calls = %d, want 1", ex.calls)
	}
	if !strings.Contains(ex.sql, "UPDATE users SET application_access = $1 WHERE id = $2") {
		t.Errorf("unexpected statement: %q", ex.sql)
	}
	// $1 = derived value, $2 = user id, in that order.
	if len(ex.args) != 2 || ex.args[0] != string(auth.AppPOS) || ex.args[1] != int64(7) {
		t.Errorf("args = %#v, want [%q 7]", ex.args, auth.AppPOS)
	}
}

// appAccessFailStore stands in for the repository after the fix: the privilege
// write was refused, so it reports the error and NO user payload at all.
type appAccessFailStore struct {
	*fakeStore
	err error
}

func (f *appAccessFailStore) CreateUser(_ context.Context, _ *auth.User, _ string) (*auth.User, error) {
	return nil, f.err
}

func (f *appAccessFailStore) UpdateUser(_ context.Context, _ *auth.User, _ *string) (*auth.User, error) {
	return nil, f.err
}

// TestCreateAndUpdateUserPropagateApplicationAccessFailure is the A4/A5 contract
// at the surface the API speaks: a refused application_access write reaches the
// caller as an error, and the returned payload cannot claim the new shell
// because there is no payload.
func TestCreateAndUpdateUserPropagateApplicationAccessFailure(t *testing.T) {
	refused := ErrApplicationAccess

	t.Run("create", func(t *testing.T) {
		s := newTestService(&appAccessFailStore{fakeStore: newFakeStore(), err: refused})
		u, err := s.CreateUser(actorCtx(auth.RoleAdmin), UserInput{
			Name: "A", Email: "a@b.co", Password: "Str0ng!P@ss1", Role: "cashier",
		})
		if !errors.Is(err, refused) {
			t.Fatalf("err = %v, want %v", err, refused)
		}
		if u != nil {
			t.Fatalf("payload must not report the value as applied, got %+v", u)
		}
	})

	t.Run("update", func(t *testing.T) {
		store := newFakeStore()
		// A non-admin victim: the admin row trips the self-escalation and
		// last-admin guards before the store is ever reached.
		store.users[9] = &auth.User{ID: 9, Name: "V", Email: "v@b.co", Role: auth.RoleSales, IsActive: true}
		s := newTestService(&appAccessFailStore{fakeStore: store, err: refused})
		u, err := s.UpdateUser(actorCtx(auth.RoleAdmin), 9, 1, UserInput{Role: "gm"})
		if !errors.Is(err, refused) {
			t.Fatalf("err = %v, want %v", err, refused)
		}
		if u != nil {
			t.Fatalf("payload must not report the value as applied, got %+v", u)
		}
	})
}

// End-to-end over the HTTP layer: a failed privilege write must not come back
// as 201/200 with a populated application_access.
func TestApplicationAccessFailureIsNotReportedAsSuccess(t *testing.T) {
	store := newFakeStore()
	store.users[9] = &auth.User{ID: 9, Name: "V", Email: "v@b.co", Role: auth.RoleSales, IsActive: true}
	h := NewHandler(newTestService(&appAccessFailStore{
		fakeStore: store,
		err:       ErrApplicationAccess,
	}))
	r := chi.NewRouter()
	r.Use(func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
			ctx := auth.ToContext(req.Context(), &auth.User{ID: 1, Role: auth.RoleAdmin}, 1)
			next.ServeHTTP(w, req.WithContext(ctx))
		})
	})
	h.RegisterRoutes(r)

	cases := []struct {
		name   string
		method string
		path   string
		body   string
	}{
		{"create", http.MethodPost, "/users", `{"name":"A","email":"a@b.co","password":"Str0ng!P@ss1","role":"cashier"}`},
		{"update", http.MethodPut, "/users/9", `{"role":"gm"}`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			req := httptest.NewRequest(tc.method, tc.path, strings.NewReader(tc.body))
			rec := httptest.NewRecorder()
			r.ServeHTTP(rec, req)

			if rec.Code == http.StatusCreated || rec.Code == http.StatusOK {
				t.Errorf("status = %d, want a failure status (body %s)", rec.Code, rec.Body.String())
			}
			if !strings.Contains(rec.Body.String(), ErrApplicationAccess.Error()) {
				t.Errorf("body must carry the failure: %s", rec.Body.String())
			}
			// The whole point of the fix: the response must not name a shell the
			// database rejected.
			if strings.Contains(rec.Body.String(), "application_access") {
				t.Errorf("response still reports application_access: %s", rec.Body.String())
			}
		})
	}
}
