const { expectRevert } = require('@openzeppelin/test-helpers');

const { expect } = require('chai');

const ImplV1 = artifacts.require('DummyImplementation');
const ImplV2 = artifacts.require('DummyImplementationV2');
const ProxyAdmin = artifacts.require('ProxyAdmin');
const TransparentUpgradeableProxy = artifacts.require('TransparentUpgradeableProxy');

contract('ProxyAdmin', function (accounts) {
  const [proxyAdminOwner, newAdmin, anotherAccount] = accounts;

  before('set implementations', async function () {
    this.implementationV1 = await ImplV1.new();
    this.implementationV2 = await ImplV2.new();
  });

  beforeEach(async function () {
    const initializeData = Buffer.from('');
    this.proxyAdmin = await ProxyAdmin.new({ from: proxyAdminOwner });
    this.proxy = await TransparentUpgradeableProxy.new(
      this.implementationV1.address,
      this.proxyAdmin.address,
      initializeData,
      { from: proxyAdminOwner },
    );
  });

  it('has an owner', async function () {
    expect(await this.proxyAdmin.owner()).to.equal(proxyAdminOwner);
  });

  describe('#getProxyAdmin', function () {
    it('returns proxyAdmin as admin of the proxy', async function () {
      const admin = await this.proxyAdmin.getProxyAdmin(this.proxy.address);
      expect(admin).to.be.equal(this.proxyAdmin.address);
    });

    it('call to invalid proxy', async function () {
      await expectRevert.unspecified(this.proxyAdmin.getProxyAdmin(this.implementationV1.address));
    });
  });

  describe('#changeProxyAdmin', function () {
    it('fails to change proxy admin if its not the proxy owner', async function () {
      await expectRevert(
        this.proxyAdmin.changeProxyAdmin(this.proxy.address, newAdmin, { from: anotherAccount }),
        'caller is not the owner',
      );
    });

    it('changes proxy admin', async function () {
      await this.proxyAdmin.changeProxyAdmin(this.proxy.address, newAdmin, { from: proxyAdminOwner });
      expect(await this.proxy.admin.call({ from: newAdmin })).to.eq(newAdmin);
    });
  });

  describe('#getProxyImplementation', function () {
    it('returns proxy implementation address', async function () {
      const implementationAddress = await this.proxyAdmin.getProxyImplementation(this.proxy.address);
      expect(implementationAddress).to.be.equal(this.implementationV1.address);
    });

    it('call to invalid proxy', async function () {
      await expectRevert.unspecified(this.proxyAdmin.getProxyImplementation(this.implementationV1.address));
    });
  });

  describe('#upgrade', function () {
    context('with unauthorized account', function () {
      it('fails to upgrade', async function () {
        await expectRevert(
          this.proxyAdmin.upgrade(this.proxy.address, this.implementationV2.address, { from: anotherAccount }),
          'caller is not the owner',
        );
      });
    });

    context('with authorized account', function () {
      it('upgrades implementation', async function () {
        await this.proxyAdmin.upgrade(this.proxy.address, this.implementationV2.address, { from: proxyAdminOwner });
        const implementationAddress = await this.proxyAdmin.getProxyImplementation(this.proxy.address);
        expect(implementationAddress).to.be.equal(this.implementationV2.address);
      });
    });
  });

  describe('#upgradeAndCall', function () {
    context('with unauthorized account', function () {
      it('fails to upgrade', async function () {
        const callData = new ImplV1('').contract.methods.initializeNonPayableWithValue(1337).encodeABI();
        await expectRevert(
          this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData,
            { from: anotherAccount },
          ),
          'caller is not the owner',
        );
      });
    });

    context('with authorized account', function () {
      context('with invalid callData', function () {
        it('fails to upgrade', async function () {
          const callData = '0x12345678';
          await expectRevert.unspecified(
            this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData,
              { from: proxyAdminOwner },
            ),
          );
        });
      });

      context('with valid callData', function () {
        it('upgrades implementation', async function () {
          const callData = new ImplV1('').contract.methods.initializeNonPayableWithValue(1337).encodeABI();
          await this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData,
            { from: proxyAdminOwner },
          );
          const implementationAddress = await this.proxyAdmin.getProxyImplementation(this.proxy.address);
          expect(implementationAddress).to.be.equal(this.implementationV2.address);
        });
      });
    });
  });
});
