package topology

import (
	"testing"
	"time"

	"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
	"github.com/seaweedfs/seaweedfs/weed/sequence"
	"github.com/seaweedfs/seaweedfs/weed/storage/needle"
	"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
	"github.com/seaweedfs/seaweedfs/weed/storage/types"
)

// A volume that grew past the limit keeps bouncing between writable and
// unwritable when the oversized mark is not refreshed on heartbeat: RegisterVolume
// sets it once, but the delta heartbeat path (ApplyVolumeChanges) never re-sets
// it, so ensureCorrectWritables sees a stale "not oversized" mark and re-adds
// the volume to writables on the next heartbeat, while RecordAssign removes it
// on the next assign. UpdateOversizedState must keep the mark current.
func TestUpdateOversizedStateKeepsOversizedVolumeUnwritable(t *testing.T) {
	layout := `
{
  "dc1":{
    "rack1":{
      "server1":{
        "volumes":[
          {"id":1, "size":4000, "replication":"000"}
        ],
        "limit":10
      }
    }
  }
}
`
	_, vl := setupPickTest(t, layout, 10000)

	// Grow the volume past the limit, as RecordAssign would see it.
	if !vl.RecordAssign(1, 9000) {
		t.Fatalf("RecordAssign should report the volume reached capacity")
	}
	if w, _ := vl.GetWritableVolumeCount(); w != 0 {
		t.Fatalf("expected 0 writable after RecordAssign, got %d", w)
	}

	// A heartbeat that reports the now-oversized volume must refresh the mark
	// so ensureCorrectWritables does not re-add it to writables. Mirrors the
	// heartbeat order: refresh, then decay, then correct.
	dn := vl.vid2location[1].list[0]
	vi, err := dn.GetVolumesById(1)
	if err != nil {
		t.Fatalf("GetVolumesById: %v", err)
	}
	oversized := vi
	oversized.Size = 12000
	vl.UpdateOversizedState(&oversized, dn)
	vl.UpdateVolumeSize(1, oversized.Size, 0, true)
	vl.EnsureCorrectWritables(&oversized)

	if w, _ := vl.GetWritableVolumeCount(); w != 0 {
		t.Fatalf("expected volume to stay unwritable after heartbeat refresh, got %d writable", w)
	}
	if !vl.vid2location[1].AnyOversized() {
		t.Fatalf("expected AnyOversized to be true after heartbeat refresh")
	}
}

// A volume that shrank back under the limit must have its oversized mark
// cleared by the heartbeat refresh, or it stays locked out of writables
// forever.
func TestUpdateOversizedStateClearsMarkWhenVolumeShrinks(t *testing.T) {
	layout := `
{
  "dc1":{
    "rack1":{
      "server1":{
        "volumes":[
          {"id":1, "size":4000, "replication":"000"}
        ],
        "limit":10
      }
    }
  }
}
`
	_, vl := setupPickTest(t, layout, 10000)

	dn := vl.vid2location[1].list[0]
	vi, err := dn.GetVolumesById(1)
	if err != nil {
		t.Fatalf("GetVolumesById: %v", err)
	}

	// Mark oversized via a heartbeat that reports a huge size.
	big := vi
	big.Size = 12000
	vl.UpdateOversizedState(&big, dn)
	if !vl.vid2location[1].AnyOversized() {
		t.Fatalf("expected AnyOversized after a huge report")
	}

	// A later heartbeat with a normal size clears the mark.
	vl.UpdateOversizedState(&vi, dn)
	if vl.vid2location[1].AnyOversized() {
		t.Fatalf("expected AnyOversized cleared after the volume shrank")
	}
}

// A volume removed for capacity must not be re-added to writables by
// ensureCorrectWritables before capacityRecoveryDelay elapses, even after its
// oversized mark is cleared by a heartbeat that reports a smaller size.
func TestEnsureCorrectWritablesHonorsRecoveryCooldown(t *testing.T) {
	layout := `
{
  "dc1":{
    "rack1":{
      "server1":{
        "volumes":[
          {"id":1, "size":4000, "replication":"000"}
        ],
        "limit":10
      }
    }
  }
}
`
	_, vl := setupPickTest(t, layout, 10000)

	// Remove the volume for capacity, as RecordAssign would.
	if !vl.RecordAssign(1, 9000) {
		t.Fatalf("RecordAssign should report the volume reached capacity")
	}
	if w, _ := vl.GetWritableVolumeCount(); w != 0 {
		t.Fatalf("expected 0 writable after RecordAssign, got %d", w)
	}

	dn := vl.vid2location[1].list[0]
	vi, err := dn.GetVolumesById(1)
	if err != nil {
		t.Fatalf("GetVolumesById: %v", err)
	}

	// Heartbeat reports the volume shrank back under the limit: the oversized
	// mark clears and effectiveSize decays, but the volume is still within
	// capacityRecoveryDelay.
	vi.Size = 4000
	vl.UpdateOversizedState(&vi, dn)
	vl.UpdateVolumeSize(1, vi.Size, 0, true)
	vl.EnsureCorrectWritables(&vi)
	if w, _ := vl.GetWritableVolumeCount(); w != 0 {
		t.Fatalf("expected volume to stay unwritable during the cooldown, got %d writable", w)
	}

	// After the cooldown, a heartbeat with the shrunken size recovers it.
	advanceSizeTrackingClock(vl, 1, capacityRecoveryDelay+time.Second)
	if !vl.UpdateVolumeSize(1, vi.Size, 0, true) {
		t.Fatalf("expected volume to recover to writable after the cooldown")
	}
	vl.EnsureCorrectWritables(&vi)
	if w, _ := vl.GetWritableVolumeCount(); w != 1 {
		t.Fatalf("expected volume writable after the cooldown, got %d", w)
	}
}

// After the cooldown elapses, a volume whose effective size is still at the
// limit must not be restored by ensureCorrectWritables: the next assign would
// remove it again.
func TestEnsureCorrectWritablesDoesNotRestoreVolumeStillAtLimit(t *testing.T) {
	layout := `
{
  "dc1":{
    "rack1":{
      "server1":{
        "volumes":[
          {"id":1, "size":8000, "replication":"000"}
        ],
        "limit":10
      }
    }
  }
}
`
	_, vl := setupPickTest(t, layout, 10000)

	// Remove the volume for capacity, as RecordAssign would. effectiveSize
	// lands at 13000, past the limit (10000); after the decay against a
	// reported size of 8000 it stays at 10500, still past the limit.
	if !vl.RecordAssign(1, 5000) {
		t.Fatalf("RecordAssign should report the volume reached capacity")
	}
	if w, _ := vl.GetWritableVolumeCount(); w != 0 {
		t.Fatalf("expected 0 writable after RecordAssign, got %d", w)
	}

	dn := vl.vid2location[1].list[0]
	vi, err := dn.GetVolumesById(1)
	if err != nil {
		t.Fatalf("GetVolumesById: %v", err)
	}

	// Heartbeat reports a size below the limit: the oversized mark clears,
	// but the decay only halves the pending estimate, so effectiveSize stays
	// past the limit and UpdateVolumeSize refuses recovery.
	vi.Size = 8000
	vl.UpdateOversizedState(&vi, dn)
	vl.UpdateVolumeSize(1, vi.Size, 0, true)
	if vl.vid2location[1].AnyOversized() {
		t.Fatalf("expected oversized mark cleared after the shrink report")
	}

	// After the cooldown, the volume is still at the limit:
	// ensureCorrectWritables must not override UpdateVolumeSize's refusal.
	advanceSizeTrackingClock(vl, 1, capacityRecoveryDelay+time.Second)
	vl.EnsureCorrectWritables(&vi)
	if w, _ := vl.GetWritableVolumeCount(); w != 0 {
		t.Fatalf("expected volume at the limit to stay unwritable, got %d writable", w)
	}
}

// A crowded volume is not a full one: it is above the growth threshold but
// still has room. One that drops out of writables while a replica is away must
// come back when the replica returns -- nothing writes to a volume that is not
// writable, so its size can never fall on its own and the lockout would be
// permanent.
func TestEnsureCorrectWritablesRestoresCrowdedVolumeAfterReplicaReturns(t *testing.T) {
	layout := `
{
  "dc1":{
    "rack1":{
      "server1":{
        "ip":"10.0.0.1",
        "volumes":[
          {"id":1, "size":9500, "replication":"001"}
        ],
        "limit":10
      },
      "server2":{
        "ip":"10.0.0.2",
        "volumes":[
          {"id":1, "size":9500, "replication":"001"}
        ],
        "limit":10
      }
    }
  }
}
`
	topo := setupWithLimit(t, layout, 10000)
	rp, _ := super_block.NewReplicaPlacementFromString("001")
	vl := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)

	// 9500 is past the growth threshold (9000) but under the limit (10000).
	vl.UpdateVolumeSize(1, 9500, 0, true)
	if _, crowded := vl.crowded[1]; !crowded {
		t.Fatalf("expected the volume to be crowded")
	}
	if w, _ := vl.GetWritableVolumeCount(); w != 1 {
		t.Fatalf("a crowded volume is still writable, got %d writable", w)
	}

	dn := vl.vid2location[1].list[1]
	vi, err := dn.GetVolumesById(1)
	if err != nil {
		t.Fatalf("GetVolumesById: %v", err)
	}
	vl.UnRegisterVolume(&vi, dn)
	if w, _ := vl.GetWritableVolumeCount(); w != 0 {
		t.Fatalf("expected 0 writable while a replica is missing, got %d", w)
	}

	// The replica comes back on the next heartbeat.
	topo.RegisterVolumeLayout(vi, dn)
	advanceSizeTrackingClock(vl, 1, 5*time.Second)
	vl.UpdateOversizedState(&vi, dn)
	vl.UpdateVolumeSize(1, vi.Size, 0, true)
	vl.EnsureCorrectWritables(&vi)
	if w, _ := vl.GetWritableVolumeCount(); w != 1 {
		t.Fatalf("expected the volume writable again, got %d", w)
	}
}

// An incremental heartbeat announces an arrival with a short message that
// carries no size. Registering from it must not clear the oversized mark the
// full heartbeat set, or the volume is handed back to the writable list until
// the next full report.
func TestIncrementalRegistrationKeepsOversizedMark(t *testing.T) {
	topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
	dc := topo.GetOrCreateDataCenter("dc1")
	rack := dc.GetOrCreateRack("rack1")
	dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", map[string]uint32{"": 25})

	rp, _ := super_block.NewReplicaPlacementFromString("000")
	vl := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)

	// A full heartbeat reports the volume past the 32 KB limit.
	topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{{
		Id:               1,
		Size:             uint64(64 * 1024),
		ReplicaPlacement: uint32(0),
		Version:          uint32(needle.GetCurrentVersion()),
	}}, dn)
	if w, _ := vl.GetWritableVolumeCount(); w != 0 {
		t.Fatalf("expected the oversized volume out of writables, got %d", w)
	}

	// The same volume is announced again as an arrival.
	topo.IncrementalSyncDataNodeRegistration([]*master_pb.VolumeShortInformationMessage{{
		Id:               1,
		ReplicaPlacement: uint32(0),
		Version:          uint32(needle.GetCurrentVersion()),
	}}, nil, dn)

	if !vl.vid2location[1].AnyOversized() {
		t.Fatalf("expected the oversized mark to survive the arrival announcement")
	}
	if w, _ := vl.GetWritableVolumeCount(); w != 0 {
		t.Fatalf("expected the oversized volume to stay out of writables, got %d", w)
	}
}
