// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package counter

import (
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"testing"
	"time"

	"golang.org/x/telemetry/internal/mmap"
	"golang.org/x/telemetry/internal/telemetry"
	"golang.org/x/telemetry/internal/testenv"
)

// this test traces the life of a counter from creation
// through two file.rotate()s, followed by a failure to rotate
func TestRotateCounters(t *testing.T) {
	testenv.SkipIfUnsupportedPlatform(t)
	t.Logf("GOOS %s GOARCH %s", runtime.GOOS, runtime.GOARCH)
	setup(t)
	defer restore()
	var f file
	defer close(&f)
	c := f.New("gophers")
	if c.ptr.count != nil {
		t.Error("new counter has non-nil ptr.count")
	}
	c.Inc() // make sure neither hits Counter.add()
	c.Inc() // second use takes a different code path
	// at this point c.file is not mapped so c's value is stored in extra.
	if c.ptr.count != nil {
		t.Error("counter without mapped file has non-nil ptr.count")
	}
	if c.file.current.Load() != nil {
		t.Error("counter has mapped file unexpectedly")
	}
	state := c.state.load()
	if state.extra() != 2 {
		// the value of c is in its extra field
		t.Errorf("got %d, expected 2", state.extra())
	}
	// Read should give the same answer
	if v, err := Read(c); err != nil || v != 2 {
		t.Errorf("Read got %d, %v, expected 2, nil", v, err)
	}
	f.rotate()
	c.Inc() // this goes through counter.add() safely
	if c.file.current.Load() == nil {
		t.Error("rotated file has no mapping")
	}
	// rotate called c.releaseLock(), moving c's value from extra to the file
	state = c.state.load()
	if state.extra() != 0 {
		t.Errorf("got %d, expected 0", state.extra())
	}
	if c.ptr.count == nil {
		t.Errorf("c has unexpected nil ptr")
	} else if c.ptr.count.Load() != 3 {
		// the value of c is in the mapped file
		t.Errorf("got %d, expected 3", c.ptr.count.Load())
	}
	// and Read should give the same result
	if v, err := Read(c); err != nil || v != 3 {
		t.Errorf("Read gave %d, %v, expected 3, nil", v, err)
	}
	// move into the future and rotate the file, remapping it
	now := getnow()
	CounterTime = func() time.Time { return now.Add(7 * 24 * time.Hour) }
	f.rotate()

	// c has value 0 in the new file
	// but c won't have a pointer until the next Inc()
	state = c.state.load()
	if c.ptr.count == nil {
		t.Errorf("c unexpedtedly has nil ptr")
	} else if state.havePtr() {
		t.Error("unexpected pointer")
	}
	if state.extra() != 0 {
		t.Errorf("got %d, expected 0", state.extra())
	}
	c.Inc()
	state = c.state.load()
	if state.extra() != 0 {
		// as expected
		t.Errorf("got %d, expected 0", state.extra())
	}
	if !state.havePtr() {
		t.Errorf("expectd havePtr")
	}
	if c.ptr.count == nil || c.ptr.count.Load() != 1 {
		t.Errorf("c has wrong value")
	}
	// add a counter
	y := f.New("counter")

	// simulate failure to remap
	oldmap := memmap
	CounterTime = func() time.Time { return now.Add(14 * 24 * time.Hour) }
	memmap = func(*os.File, *mmap.Data) (mmap.Data, error) { return mmap.Data{}, fmt.Errorf("too bad") }
	f.rotate()
	memmap = oldmap

	// no mapping
	if f.current.Load() != nil {
		t.Errorf("unexpected mapping")
	}
	c.Inc()
	// c should not have a pointer, but its internal
	// count should have been incremented
	if c.ptr.count != nil {
		t.Error("expected nil ptr")
	}
	if c.state.load().extra() != 1 {
		t.Errorf("got %d, but expected extra to be 1", c.state.load().extra())
	}
	// make sure a new counter doesn't fault
	x := f.New("newcounter")
	x.Inc()
	if x.state.load().extra() != 1 {
		t.Errorf("got %d, but expected extra to be 1", c.state.load().extra())
	}
	// make sure an existing unused counter doesn't fault
	// (it's incremented, but not visible externally)
	y.Inc()
	if y.state.load().extra() != 1 {
		t.Errorf("got %d, but expected extra to be 1", c.state.load().extra())
	}
}

// return the current date according to counterTime()
func getnow() time.Time {
	year, month, day := CounterTime().Date()
	now := time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
	return now
}
func TestRotate(t *testing.T) {
	testenv.SkipIfUnsupportedPlatform(t)

	t.Logf("GOOS %s GOARCH %s", runtime.GOOS, runtime.GOARCH)
	now := getnow()
	setup(t)
	defer restore()
	// pretend something was uploaded
	os.WriteFile(filepath.Join(telemetry.Default.UploadDir(), "anything"), []byte{}, 0666)
	var f file
	defer close(&f)
	c := f.New("gophers")
	c.Inc()
	var modified int
	for i := 0; i < 2; i++ {
		// nothing should change on the second rotate
		f.rotate()
		fi, err := os.ReadDir(telemetry.Default.LocalDir())
		if err != nil || len(fi) != 2 {
			t.Fatalf("err=%v, len(fi) = %d, want 2", err, len(fi))
		}
		x := fi[0].Name()
		y := x[len(x)-len("2006-01-02")-len(".v1.count") : len(x)-len(".v1.count")]
		us, err := time.ParseInLocation("2006-01-02", y, time.UTC)
		if err != nil {
			t.Fatal(err)
		}
		// we expect today's date?
		if us != now {
			t.Errorf("us = %v, want %v, i=%d y=%s", us, now, i, y)
		}
		fd, err := os.Open(filepath.Join(telemetry.Default.LocalDir(), fi[0].Name()))
		if err != nil {
			t.Fatal(err)
		}
		stat, err := fd.Stat()
		if err != nil {
			t.Fatal(err)
		}
		mt := stat.ModTime().Nanosecond()
		if modified == 0 {
			modified = mt
		}
		if modified != mt {
			t.Errorf("modified = %v, want %v", mt, modified)
		}
		fd.Close()
	}
	CounterTime = func() time.Time { return now.Add(7 * 24 * time.Hour) }
	f.rotate()
	fi, err := os.ReadDir(telemetry.Default.LocalDir())
	if err != nil || len(fi) != 3 {
		t.Fatalf("err=%v, len(fi) = %d, want 3", err, len(fi))
	}
}

// These were useful while debugging failed mapping
func (s *counterState) String() string {
	if s == nil {
		return "nil"
	}
	return s.load().String()
}

func (b counterStateBits) String() string {
	rdrs := b.readers()
	locked := b.locked()
	if locked {
		rdrs = 0 // rdrs == 1<<30 - 1
	}
	havePtr := b&stateHavePtr != 0
	extra := uint64(b&stateExtra) >> stateExtraShift
	return fmt.Sprintf("rdrs:0x%x locked:%v\thavePtr:%v\textra:%d", rdrs, locked, havePtr, extra)
}
