// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {Vector2D, Rect2D, type Bounds2D, Transform2D} from './geom';

describe('Vector2D', () => {
  test('add', () => {
    const vector1 = new Vector2D({x: 1, y: 2});
    const vector2 = new Vector2D({x: 3, y: 4});
    const result = vector1.add(vector2);
    expect(result.x).toBe(4);
    expect(result.y).toBe(6);
  });

  test('sub', () => {
    const vector1 = new Vector2D({x: 5, y: 7});
    const vector2 = new Vector2D({x: 2, y: 3});
    const result = vector1.sub(vector2);
    expect(result.x).toBe(3);
    expect(result.y).toBe(4);
  });

  test('scale', () => {
    const vector = new Vector2D({x: 2, y: 3});
    const result = vector.scale(2);
    expect(result.x).toBe(4);
    expect(result.y).toBe(6);
  });
});

describe('Rect2D', () => {
  test('asPoint', () => {
    const rect = new Rect2D({left: 1, top: 2, right: 3, bottom: 4});
    expect(rect).toMatchObject({x: 1, y: 2});
  });

  test('asSize', () => {
    const rect = new Rect2D({left: 1, top: 2, right: 3, bottom: 8});
    expect(rect).toMatchObject({width: 2, height: 6});
  });

  test('intersect', () => {
    const a = new Rect2D({left: 1, top: 1, right: 4, bottom: 4});
    const b = {left: 2, top: 2, right: 5, bottom: 5};
    const result = a.intersect(b);
    expect(result).toMatchObject({left: 2, top: 2, right: 4, bottom: 4});
    // Note: Non-overlapping rects are UB and thus not tested
    // TODO(stevegolton): Work out what to do here.
  });

  test('expand', () => {
    const rect = new Rect2D({left: 1, top: 1, right: 3, bottom: 3});
    const result = rect.expand(1);
    expect(result).toMatchObject({left: 0, top: 0, right: 4, bottom: 4});
  });

  test('expand 2d', () => {
    const rect = new Rect2D({left: 1, top: 1, right: 3, bottom: 3});
    const result = rect.expand({width: 1, height: 2});
    expect(result).toMatchObject({left: 0, top: -1, right: 4, bottom: 5});
  });

  test('reframe', () => {
    const rect = new Rect2D({left: 2, top: 2, right: 5, bottom: 5});
    const result = rect.reframe({x: 1, y: 1});
    expect(result).toMatchObject({left: 1, top: 1, right: 4, bottom: 4});
  });

  test('size', () => {
    const rect = new Rect2D({left: 1, top: 1, right: 4, bottom: 3});
    expect(rect).toMatchObject({width: 3, height: 2});
  });

  it('translate', () => {
    const rect = new Rect2D({left: 2, top: 2, right: 5, bottom: 5});
    const result = rect.translate({x: 3, y: 4});
    expect(result).toMatchObject({left: 5, top: 6, right: 8, bottom: 9});
  });

  it('contains', () => {
    const outerRect = new Rect2D({left: 0, top: 0, right: 10, bottom: 10});
    const innerRect: Bounds2D = {left: 2, top: 2, right: 8, bottom: 8};
    expect(outerRect.contains(innerRect)).toBe(true);

    const nonContainedRect: Bounds2D = {left: 2, top: 2, right: 12, bottom: 8};
    expect(outerRect.contains(nonContainedRect)).toBe(false);
  });

  test('fromPointAndSize', () => {
    const rect = Rect2D.fromPointAndSize({
      x: 10,
      y: 20,
      width: 100,
      height: 50,
    });

    expect(rect.left).toBe(10);
    expect(rect.top).toBe(20);
    expect(rect.right).toBe(110);
    expect(rect.bottom).toBe(70);
    expect(rect.width).toBe(100);
    expect(rect.height).toBe(50);
  });

  test('fromPoints', () => {
    const rect = Rect2D.fromPoints({x: 0, y: 0}, {x: 100, y: 100});

    expect(rect.left).toBe(0);
    expect(rect.top).toBe(0);
    expect(rect.right).toBe(100);
    expect(rect.bottom).toBe(100);
  });

  test('fromPoints reversed', () => {
    const rect = Rect2D.fromPoints({x: 100, y: 100}, {x: 0, y: 0});

    expect(rect.left).toBe(0);
    expect(rect.top).toBe(0);
    expect(rect.right).toBe(100);
    expect(rect.bottom).toBe(100);
  });

  describe('containsPoint', () => {
    let rect: Rect2D;

    beforeEach(() => {
      rect = new Rect2D({left: 10, top: 20, right: 110, bottom: 70});
    });

    test('inside the rectangle', () => {
      expect(rect.containsPoint({x: 50, y: 50})).toBe(true);
    });

    test('outside the rectangle', () => {
      expect(rect.containsPoint({x: 5, y: 50})).toBe(false); // Left of rect
      expect(rect.containsPoint({x: 50, y: 75})).toBe(false); // Below rect
      expect(rect.containsPoint({x: 150, y: 50})).toBe(false); // Right of rect
      expect(rect.containsPoint({x: 50, y: 15})).toBe(false); // Above rect
    });

    test('boundary case', () => {
      expect(rect.containsPoint({x: 10, y: 20})).toBe(true); // Top-left corner
      expect(rect.containsPoint({x: 110, y: 20})).toBe(false); // On right edge
      expect(rect.containsPoint({x: 10, y: 70})).toBe(false); // On bottom edge
    });
  });
});

describe('Transform2D', () => {
  test('Identity', () => {
    expect(Transform2D.Identity).toEqual({
      offsetX: 0,
      offsetY: 0,
      scaleX: 1,
      scaleY: 1,
    });
  });

  test('compose', () => {
    const a: Transform2D = {
      offsetX: 10,
      offsetY: 20,
      scaleX: 2,
      scaleY: 3,
    };
    const b: Partial<Transform2D> = {
      offsetX: 5,
      offsetY: 5,
      scaleX: 2,
      scaleY: 2,
    };
    const result = Transform2D.compose(a, b);
    expect(result).toEqual({
      offsetX: 20,
      offsetY: 35,
      scaleX: 4,
      scaleY: 6,
    });
  });

  test('compose with empty partial', () => {
    const a: Transform2D = {
      offsetX: 10,
      offsetY: 20,
      scaleX: 2,
      scaleY: 3,
    };
    const result = Transform2D.compose(a, {});
    expect(result).toEqual(a);
  });
});
