package health

import (
	"context"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"
)

// fakeDoer returns a canned response and error for every request.
type fakeDoer struct {
	resp *http.Response
	err  error
}

func (f *fakeDoer) Do(*http.Request) (*http.Response, error) { return f.resp, f.err }

// mkResp builds a response with a status, optional Retry-After header and body.
func mkResp(status int, retryAfter, body string) *http.Response {
	h := http.Header{}
	if retryAfter != "" {
		h.Set("Retry-After", retryAfter)
	}
	return &http.Response{StatusCode: status, Header: h, Body: io.NopCloser(strings.NewReader(body))}
}

// run sends one request for the Bot API method through a fresh observer and returns the reporter's
// resulting snapshot. The 429 cases carry no Retry-After, so the back-off returns before any wait.
func run(method string, resp *http.Response, err error) *Reporter {
	r := NewReporter()
	req := httptest.NewRequest(http.MethodPost, "https://api.telegram.org/bot123/"+method, nil)
	_, _ = r.Wrap(&fakeDoer{resp: resp, err: err}).Do(req)
	return r
}

func TestObserverClassifies(t *testing.T) {
	cases := []struct {
		name          string
		method        string
		resp          *http.Response
		err           error
		wantConnect   uint64
		wantAPI       uint64
		wantRate      uint64
		wantLastOKSet bool
	}{
		{"poll ok stamps liveness", "getUpdates", mkResp(200, "", `{"ok":true}`), nil, 0, 0, 0, true},
		{"send ok stamps liveness", "sendMessage", mkResp(200, "", `{"ok":true}`), nil, 0, 0, 0, true},
		{"poll 5xx is a connect failure", "getUpdates", mkResp(502, "", ""), nil, 1, 0, 0, false},
		{"send 5xx is an api error", "sendMessage", mkResp(500, "", ""), nil, 0, 1, 0, false},
		{"429 is rate limited, not an error", "sendMessage", mkResp(429, "", `{"ok":false}`), nil, 0, 0, 1, false},
		{"4xx other than 429 is not counted", "sendMessage", mkResp(403, "", ""), nil, 0, 0, 0, false},
		{"poll transport error is a connect failure", "getUpdates", nil, errors.New("dial"), 1, 0, 0, false},
		{"send transport error is an api error", "sendMessage", nil, errors.New("dial"), 0, 1, 0, false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			s := run(tc.method, tc.resp, tc.err).Snapshot()
			if s.GetConnectFailures() != tc.wantConnect || s.GetApiErrors() != tc.wantAPI || s.GetRateLimited() != tc.wantRate {
				t.Errorf("counters = connect %d api %d rate %d, want %d/%d/%d",
					s.GetConnectFailures(), s.GetApiErrors(), s.GetRateLimited(), tc.wantConnect, tc.wantAPI, tc.wantRate)
			}
			if (s.GetLastOkUnix() > 0) != tc.wantLastOKSet {
				t.Errorf("last_ok set = %v, want %v", s.GetLastOkUnix() > 0, tc.wantLastOKSet)
			}
		})
	}
}

// TestObserverIgnoresShutdownTransportError checks a transport error under a cancelled context (a
// shutdown) is not counted as a Bot API failure.
func TestObserverIgnoresShutdownTransportError(t *testing.T) {
	r := NewReporter()
	req := httptest.NewRequest(http.MethodPost, "https://api.telegram.org/bot123/getUpdates", nil)
	ctx, cancel := context.WithCancel(req.Context())
	cancel()
	_, _ = r.Wrap(&fakeDoer{err: errors.New("dial")}).Do(req.WithContext(ctx))
	if s := r.Snapshot(); s.GetConnectFailures() != 0 {
		t.Errorf("shutdown transport error must not count, got connect=%d", s.GetConnectFailures())
	}
}

func TestRetryAfter(t *testing.T) {
	cases := []struct {
		header string
		body   string
		want   time.Duration
	}{
		{"5", "", 5 * time.Second},
		{"", `{"parameters":{"retry_after":7}}`, 7 * time.Second},
		{"3", `{"parameters":{"retry_after":9}}`, 3 * time.Second}, // header wins
		{"", `{"ok":false}`, 0},
		{"0", "", 0},
		{"", "not json", 0},
	}
	for _, tc := range cases {
		if got := retryAfter(tc.header, []byte(tc.body)); got != tc.want {
			t.Errorf("retryAfter(%q, %q) = %v, want %v", tc.header, tc.body, got, tc.want)
		}
	}
}

// TestSnapshotCommit checks Commit clears only the sent deltas, so counts accrued between the
// snapshot and the send survive into the next report.
func TestSnapshotCommit(t *testing.T) {
	r := NewReporter()
	r.apiErrors.Add(3)
	snap := r.Snapshot()
	if snap.GetApiErrors() != 3 {
		t.Fatalf("snapshot api_errors = %d, want 3", snap.GetApiErrors())
	}
	r.apiErrors.Add(2) // accrues after the snapshot
	r.Commit(snap)
	if got := r.Snapshot().GetApiErrors(); got != 2 {
		t.Errorf("after commit api_errors = %d, want 2 (the post-snapshot accrual)", got)
	}
}
