package main

import (
	"crypto/ed25519"
	"crypto/rand"
	"encoding/base32"
	"encoding/binary"
	"encoding/json"
	"os"
	"strings"
	"testing"
	"time"
)

// These two keys were issued by the real licensekit with the real signing
// key. They are the contract between the private issuing tool and this
// verifier: if either half of the format drifts, these stop verifying and
// this test says so before a paying customer does.
//
// Being in a test file puts two genuine keys in public view, which is fine:
// they are made out to a fixture, and a licence key here is a receipt, not a
// password -- nothing anywhere checks a key against a list of sold ones,
// because there is no server to hold such a list.
const (
	fixtureOneTime  = "TL1-AEBAG-UGXKJ-CCORT-JPB2H-K4TFE-BEG63-DEMVZ-CAPDG-NF4HI-5LSMV-AHIZL-DNBWG-643PM-Z2C4Y-3PNU7-MZKRX-EFBWH-WN2IM-LKDAI-FOI4E-UNR45-FUNPS-52WDI-HSUGQ-POBEL-GF55W-EXZHN-65FHZ-77BJ7-WQMRJ-LAIQO-HU233-EHCSY-H7CQW-2SNNM-PBY"
	fixtureLifetime = "TL1-AEBQK-UGXAA-AABPZ-6QURK-7NVLC-KVYZM-NCQMK-WO76W-VXRIT-3FIIM-WXB2B-F5QJN-HAMMX-JTP4L-4GDYY-NGWZE-O7BAZ-4RTHQ-FE6OW-3NBE7-OJXCY-Y73UJ-5REQC-Q"
)

func TestAKeyIssuedByTheRealToolVerifies(t *testing.T) {
	lic, err := verifyTechlosoftKey(fixtureOneTime)
	if err != nil {
		t.Fatalf("fixture one-time key did not verify: %v", err)
	}
	if lic.Plan != "one-time" || lic.Seats != 3 {
		t.Fatalf("wrong plan/seats: %+v", lic)
	}
	if lic.Name != "Fixture Holder <fixture@techlosoft.com>" {
		t.Fatalf("wrong licensed-to: %q", lic.Name)
	}
	if got := lic.UpdatesUntil.Format("2006-01-02"); got != "2027-08-30" {
		t.Fatalf("wrong updates-until: %s", got)
	}

	life, err := verifyTechlosoftKey(fixtureLifetime)
	if err != nil {
		t.Fatalf("fixture lifetime key did not verify: %v", err)
	}
	if life.Plan != "lifetime" || life.Seats != 5 || life.Name != "" {
		t.Fatalf("wrong lifetime fields: %+v", life)
	}
	if !life.UpdatesUntil.IsZero() {
		t.Fatalf("lifetime key should have no updates end date, got %v", life.UpdatesUntil)
	}
}

func TestTheKeySurvivesWhatMailClientsDoToIt(t *testing.T) {
	// Lowercased, hyphens gone, wrapped across lines, padded with spaces:
	// everything a copy-paste out of an email does.
	mangled := strings.ToLower(strings.ReplaceAll(fixtureOneTime, "-", " \n "))
	if _, err := verifyTechlosoftKey("  " + mangled + "\t"); err != nil {
		t.Fatalf("mangled-but-complete key should verify: %v", err)
	}
}

func TestOneChangedCharacterIsRejected(t *testing.T) {
	// Flip one base32 character somewhere in the middle. The alphabet is
	// A-Z2-7, so swapping between two letters keeps it decodable and the
	// failure is the signature check, not the decoder.
	i := strings.LastIndex(fixtureOneTime, "K")
	flipped := fixtureOneTime[:i] + "M" + fixtureOneTime[i+1:]
	if _, err := verifyTechlosoftKey(flipped); err == nil {
		t.Fatal("a key with one changed character verified")
	}
	if _, err := verifyTechlosoftKey(fixtureOneTime[:len(fixtureOneTime)/2]); err == nil {
		t.Fatal("half a key verified")
	}
	if _, err := verifyTechlosoftKey("XX9" + fixtureOneTime[3:]); err == nil {
		t.Fatal("a key with the wrong prefix verified")
	}
	if _, err := verifyTechlosoftKey(""); err == nil {
		t.Fatal("an empty string verified")
	}
}

// sign builds a key the way licensekit does, for round-trip tests with a
// throwaway pair. This is the third writing-down of the wire format, on
// purpose: if any of the three disagrees, some test here fails.
func testSign(priv ed25519.PrivateKey, plan, seats byte, issued, updates uint16, name string) string {
	payload := []byte{licenceFormatVersion, plan, seats}
	payload = binary.BigEndian.AppendUint16(payload, issued)
	payload = binary.BigEndian.AppendUint16(payload, updates)
	payload = append(payload, byte(len(name)))
	payload = append(payload, name...)
	sig := ed25519.Sign(priv, append([]byte(licenceContext), payload...))
	return licenceKeyPrefix +
		base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(append(payload, sig...))
}

func TestRoundTripWithAThrowawayKeypair(t *testing.T) {
	pub, priv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatal(err)
	}
	issued := uint16(time.Now().Unix() / 86400)
	key := testSign(priv, 1, 3, issued, issued+365, "someone@example.com")
	lic, err := verifyLicenceKey(pub, key)
	if err != nil {
		t.Fatalf("round trip failed: %v", err)
	}
	if lic.Plan != "yearly" || lic.Seats != 3 || lic.Name != "someone@example.com" {
		t.Fatalf("round trip mangled the fields: %+v", lic)
	}
	// A key signed by a different private key must not verify against the
	// shipped public key, however well-formed it is.
	if _, err := verifyTechlosoftKey(key); err == nil {
		t.Fatal("a key signed by a stranger's keypair verified against the real public key")
	}
	// An inner length byte that disagrees with the payload is caught even
	// when the signature over the malformed bytes is genuine.
	bad := testSign(priv, 1, 3, issued, 0, "x")
	raw, _ := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(bad[3:])
	raw[7] = 9 // claims a 9-byte name; there is 1
	resigned := append(raw[:len(raw)-ed25519.SignatureSize:len(raw)-ed25519.SignatureSize],
		ed25519.Sign(priv, append([]byte(licenceContext), raw[:len(raw)-ed25519.SignatureSize]...))...)
	badKey := licenceKeyPrefix + base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(resigned)
	if _, err := verifyLicenceKey(pub, badKey); err == nil {
		t.Fatal("a signed-but-self-contradictory key verified")
	}
}

func TestActivationWritesTheLicenceWhereTheRulesAllow(t *testing.T) {
	dir := t.TempDir()
	r, err := confirmRoot(dir + "/Techlosoft")
	if err != nil {
		t.Fatal(err)
	}
	lic, err := verifyTechlosoftKey(fixtureLifetime)
	if err != nil {
		t.Fatal(err)
	}
	stored := StoredLicence{Key: fixtureLifetime, ActivatedAt: time.Now().UTC(), Licence: lic}
	if !strings.HasPrefix(r.licencePath(), r.Path) {
		t.Fatalf("licence file %s is outside the install root %s, which breaks the one-directory promise", r.licencePath(), r.Path)
	}
	rawJSON, err := json.MarshalIndent(stored, "", "  ")
	if err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(r.licencePath(), rawJSON, 0o644); err != nil {
		t.Fatal(err)
	}
	back, err := r.loadLicence()
	if err != nil {
		t.Fatal(err)
	}
	if back == nil || back.Plan != "lifetime" || back.Key != fixtureLifetime {
		t.Fatalf("licence did not round-trip through disk: %+v", back)
	}
	// A missing file is "no licence", not an error.
	empty, err := confirmRoot(dir + "/Empty")
	if err != nil {
		t.Fatal(err)
	}
	got, err := empty.loadLicence()
	if err != nil || got != nil {
		t.Fatalf("an absent licence file should be (nil, nil), got (%v, %v)", got, err)
	}
}
